Compare commits

..

No commits in common. "87261f3fb95da92d74d72d057fc0f1e9819f16a7" and "2c6cc0b8c982b3d49a5b208f75e15b2269584312" have entirely different histories.

39 changed files with 448 additions and 558 deletions

View File

@ -372,15 +372,6 @@ var (
Value: "", Value: "",
Usage: "Group Claim value for restricted users", Usage: "Group Claim value for restricted users",
}, },
cli.StringFlag{
Name: "group-team-map",
Value: "",
Usage: "JSON mapping between groups and org teams",
},
cli.BoolFlag{
Name: "group-team-map-removal",
Usage: "Activate automatic team membership removal depending on groups",
},
} }
microcmdAuthUpdateOauth = cli.Command{ microcmdAuthUpdateOauth = cli.Command{
@ -862,8 +853,6 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
GroupClaimName: c.String("group-claim-name"), GroupClaimName: c.String("group-claim-name"),
AdminGroup: c.String("admin-group"), AdminGroup: c.String("admin-group"),
RestrictedGroup: c.String("restricted-group"), RestrictedGroup: c.String("restricted-group"),
GroupTeamMap: c.String("group-team-map"),
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
} }
} }
@ -946,12 +935,6 @@ func runUpdateOauth(c *cli.Context) error {
if c.IsSet("restricted-group") { if c.IsSet("restricted-group") {
oAuth2Config.RestrictedGroup = c.String("restricted-group") oAuth2Config.RestrictedGroup = c.String("restricted-group")
} }
if c.IsSet("group-team-map") {
oAuth2Config.GroupTeamMap = c.String("group-team-map")
}
if c.IsSet("group-team-map-removal") {
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
// update custom URL mapping // update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{} customURLMapping := &oauth2.CustomURLMapping{}

View File

@ -137,8 +137,6 @@ Admin operations:
- `--group-claim-name`: Claim name providing group names for this source. (Optional) - `--group-claim-name`: Claim name providing group names for this source. (Optional)
- `--admin-group`: Group Claim value for administrator users. (Optional) - `--admin-group`: Group Claim value for administrator users. (Optional)
- `--restricted-group`: Group Claim value for restricted users. (Optional) - `--restricted-group`: Group Claim value for restricted users. (Optional)
- `--group-team-map`: JSON mapping between groups and org teams. (Optional)
- `--group-team-map-removal`: Activate automatic team membership removal depending on groups. (Optional)
- Examples: - Examples:
- `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE` - `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE`
- `update-oauth`: - `update-oauth`:

View File

@ -110,14 +110,22 @@ func (org *Organization) CanCreateOrgRepo(uid int64) (bool, error) {
return CanCreateOrgRepo(db.DefaultContext, org.ID, uid) return CanCreateOrgRepo(db.DefaultContext, org.ID, uid)
} }
// GetTeam returns named team of organization. func (org *Organization) getTeam(ctx context.Context, name string) (*Team, error) {
func (org *Organization) GetTeam(ctx context.Context, name string) (*Team, error) {
return GetTeam(ctx, org.ID, name) return GetTeam(ctx, org.ID, name)
} }
// GetTeam returns named team of organization.
func (org *Organization) GetTeam(name string) (*Team, error) {
return org.getTeam(db.DefaultContext, name)
}
func (org *Organization) getOwnerTeam(ctx context.Context) (*Team, error) {
return org.getTeam(ctx, OwnerTeamName)
}
// GetOwnerTeam returns owner team of organization. // GetOwnerTeam returns owner team of organization.
func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) { func (org *Organization) GetOwnerTeam() (*Team, error) {
return org.GetTeam(ctx, OwnerTeamName) return org.getOwnerTeam(db.DefaultContext)
} }
// FindOrgTeams returns all teams of a given organization // FindOrgTeams returns all teams of a given organization
@ -334,7 +342,7 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) {
} }
// GetOrgByName returns organization by given name. // GetOrgByName returns organization by given name.
func GetOrgByName(ctx context.Context, name string) (*Organization, error) { func GetOrgByName(name string) (*Organization, error) {
if len(name) == 0 { if len(name) == 0 {
return nil, ErrOrgNotExist{0, name} return nil, ErrOrgNotExist{0, name}
} }
@ -342,7 +350,7 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
LowerName: strings.ToLower(name), LowerName: strings.ToLower(name),
Type: user_model.UserTypeOrganization, Type: user_model.UserTypeOrganization,
} }
has, err := db.GetEngine(ctx).Get(u) has, err := db.GetEngine(db.DefaultContext).Get(u)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {

View File

@ -61,28 +61,28 @@ func TestUser_IsOrgMember(t *testing.T) {
func TestUser_GetTeam(t *testing.T) { func TestUser_GetTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
team, err := org.GetTeam(db.DefaultContext, "team1") team, err := org.GetTeam("team1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID) assert.Equal(t, org.ID, team.OrgID)
assert.Equal(t, "team1", team.LowerName) assert.Equal(t, "team1", team.LowerName)
_, err = org.GetTeam(db.DefaultContext, "does not exist") _, err = org.GetTeam("does not exist")
assert.True(t, organization.IsErrTeamNotExist(err)) assert.True(t, organization.IsErrTeamNotExist(err))
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
_, err = nonOrg.GetTeam(db.DefaultContext, "team") _, err = nonOrg.GetTeam("team")
assert.True(t, organization.IsErrTeamNotExist(err)) assert.True(t, organization.IsErrTeamNotExist(err))
} }
func TestUser_GetOwnerTeam(t *testing.T) { func TestUser_GetOwnerTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
team, err := org.GetOwnerTeam(db.DefaultContext) team, err := org.GetOwnerTeam()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID) assert.Equal(t, org.ID, team.OrgID)
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
_, err = nonOrg.GetOwnerTeam(db.DefaultContext) _, err = nonOrg.GetOwnerTeam()
assert.True(t, organization.IsErrTeamNotExist(err)) assert.True(t, organization.IsErrTeamNotExist(err))
} }
@ -115,15 +115,15 @@ func TestUser_GetMembers(t *testing.T) {
func TestGetOrgByName(t *testing.T) { func TestGetOrgByName(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org, err := organization.GetOrgByName(db.DefaultContext, "user3") org, err := organization.GetOrgByName("user3")
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 3, org.ID) assert.EqualValues(t, 3, org.ID)
assert.Equal(t, "user3", org.Name) assert.Equal(t, "user3", org.Name)
_, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual _, err = organization.GetOrgByName("user2") // user2 is an individual
assert.True(t, organization.IsErrOrgNotExist(err)) assert.True(t, organization.IsErrOrgNotExist(err))
_, err = organization.GetOrgByName(db.DefaultContext, "") // corner case _, err = organization.GetOrgByName("") // corner case
assert.True(t, organization.IsErrOrgNotExist(err)) assert.True(t, organization.IsErrOrgNotExist(err))
} }

View File

@ -1,22 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
)
func UnmarshalGroupTeamMapping(raw string) (map[string]map[string][]string, error) {
groupTeamMapping := make(map[string]map[string][]string)
if raw == "" {
return groupTeamMapping, nil
}
err := json.Unmarshal([]byte(raw), &groupTeamMapping)
if err != nil {
log.Error("Failed to unmarshal group team mapping: %v", err)
return nil, err
}
return groupTeamMapping, nil
}

View File

@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth"
) )
// APIContext is a specific context for API service // APIContext is a specific context for API service
@ -214,6 +215,35 @@ func (ctx *APIContext) CheckForOTP() {
} }
} }
// APIAuth converts auth_service.Auth as a middleware
func APIAuth(authMethod auth_service.Method) func(*APIContext) {
return func(ctx *APIContext) {
// Get user from session if logged in.
var err error
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
return
}
if ctx.Doer != nil {
if ctx.Locale.Language() != ctx.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName
ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data["SignedUser"] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["SignedUserName"] = ctx.Doer.Name
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
ctx.Data["SignedUserName"] = ""
}
}
}
// APIContexter returns apicontext as middleware // APIContexter returns apicontext as middleware
func APIContexter() func(http.Handler) http.Handler { func APIContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {

View File

@ -36,6 +36,7 @@ import (
"code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/auth"
"gitea.com/go-chi/cache" "gitea.com/go-chi/cache"
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
@ -658,6 +659,37 @@ func getCsrfOpts() CsrfOptions {
} }
} }
// Auth converts auth.Auth as a middleware
func Auth(authMethod auth.Method) func(*Context) {
return func(ctx *Context) {
var err error
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err)
ctx.Error(http.StatusUnauthorized, "Verify")
return
}
if ctx.Doer != nil {
if ctx.Locale.Language() != ctx.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth.BasicMethodName
ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data["SignedUser"] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["SignedUserName"] = ctx.Doer.Name
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
ctx.Data["SignedUserName"] = ""
// ensure the session uid is deleted
_ = ctx.Session.Delete("uid")
}
}
}
// Contexter initializes a classic context for a request. // Contexter initializes a classic context for a request.
func Contexter(ctx context.Context) func(next http.Handler) http.Handler { func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
_, rnd := templates.HTMLRenderer(ctx) _, rnd := templates.HTMLRenderer(ctx)

View File

@ -80,7 +80,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
orgName := ctx.Params(":org") orgName := ctx.Params(":org")
var err error var err error
ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) ctx.Org.Organization, err = organization.GetOrgByName(orgName)
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(orgName) redirectUserID, err := user_model.LookupUserRedirect(orgName)

View File

@ -20,12 +20,11 @@ type BlamePart struct {
// BlameReader returns part of file blame one by one // BlameReader returns part of file blame one by one
type BlameReader struct { type BlameReader struct {
cmd *Command cmd *Command
output io.WriteCloser output io.WriteCloser
reader io.ReadCloser reader io.ReadCloser
bufferedReader *bufio.Reader done chan error
done chan error lastSha *string
lastSha *string
} }
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
@ -34,6 +33,8 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
func (r *BlameReader) NextPart() (*BlamePart, error) { func (r *BlameReader) NextPart() (*BlamePart, error) {
var blamePart *BlamePart var blamePart *BlamePart
reader := bufio.NewReader(r.reader)
if r.lastSha != nil { if r.lastSha != nil {
blamePart = &BlamePart{*r.lastSha, make([]string, 0)} blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
} }
@ -43,7 +44,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
var err error var err error
for err != io.EOF { for err != io.EOF {
line, isPrefix, err = r.bufferedReader.ReadLine() line, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return blamePart, err return blamePart, err
} }
@ -65,7 +66,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
r.lastSha = &sha1 r.lastSha = &sha1
// need to munch to end of line... // need to munch to end of line...
for isPrefix { for isPrefix {
_, isPrefix, err = r.bufferedReader.ReadLine() _, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return blamePart, err return blamePart, err
} }
@ -80,7 +81,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
// need to munch to end of line... // need to munch to end of line...
for isPrefix { for isPrefix {
_, isPrefix, err = r.bufferedReader.ReadLine() _, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return blamePart, err return blamePart, err
} }
@ -95,7 +96,6 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
// Close BlameReader - don't run NextPart after invoking that // Close BlameReader - don't run NextPart after invoking that
func (r *BlameReader) Close() error { func (r *BlameReader) Close() error {
err := <-r.done err := <-r.done
r.bufferedReader = nil
_ = r.reader.Close() _ = r.reader.Close()
_ = r.output.Close() _ = r.output.Close()
return err return err
@ -126,13 +126,10 @@ func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*B
done <- err done <- err
}(cmd, repoPath, stdout, done) }(cmd, repoPath, stdout, done)
bufferedReader := bufio.NewReader(reader)
return &BlameReader{ return &BlameReader{
cmd: cmd, cmd: cmd,
output: stdout, output: stdout,
reader: reader, reader: reader,
bufferedReader: bufferedReader, done: done,
done: done,
}, nil }, nil
} }

View File

@ -28,7 +28,7 @@ func TestReadingBlameOutput(t *testing.T) {
}, },
{ {
"f32b0a9dfd09a60f616f29158f772cedd89942d2", "f32b0a9dfd09a60f616f29158f772cedd89942d2",
[]string{"", "Do not make any changes to this repo it is used for unit testing"}, []string{},
}, },
} }

View File

@ -4,10 +4,7 @@
package metrics package metrics
import ( import (
"runtime"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/setting"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
@ -20,7 +17,6 @@ type Collector struct {
Accesses *prometheus.Desc Accesses *prometheus.Desc
Actions *prometheus.Desc Actions *prometheus.Desc
Attachments *prometheus.Desc Attachments *prometheus.Desc
BuildInfo *prometheus.Desc
Comments *prometheus.Desc Comments *prometheus.Desc
Follows *prometheus.Desc Follows *prometheus.Desc
HookTasks *prometheus.Desc HookTasks *prometheus.Desc
@ -66,16 +62,6 @@ func NewCollector() Collector {
"Number of Attachments", "Number of Attachments",
nil, nil, nil, nil,
), ),
BuildInfo: prometheus.NewDesc(
namespace+"build_info",
"Build information",
[]string{
"goarch",
"goos",
"goversion",
"version",
}, nil,
),
Comments: prometheus.NewDesc( Comments: prometheus.NewDesc(
namespace+"comments", namespace+"comments",
"Number of Comments", "Number of Comments",
@ -209,7 +195,6 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.Accesses ch <- c.Accesses
ch <- c.Actions ch <- c.Actions
ch <- c.Attachments ch <- c.Attachments
ch <- c.BuildInfo
ch <- c.Comments ch <- c.Comments
ch <- c.Follows ch <- c.Follows
ch <- c.HookTasks ch <- c.HookTasks
@ -256,15 +241,6 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
prometheus.GaugeValue, prometheus.GaugeValue,
float64(stats.Counter.Attachment), float64(stats.Counter.Attachment),
) )
ch <- prometheus.MustNewConstMetric(
c.BuildInfo,
prometheus.GaugeValue,
1,
runtime.GOARCH,
runtime.GOOS,
runtime.Version(),
setting.AppVer,
)
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.Comments, c.Comments,
prometheus.GaugeValue, prometheus.GaugeValue,

View File

@ -49,7 +49,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization")
// Check Owner team. // Check Owner team.
ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) ownerTeam, err := org.GetOwnerTeam()
assert.NoError(t, err, "GetOwnerTeam") assert.NoError(t, err, "GetOwnerTeam")
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
@ -63,7 +63,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
} }
} }
// Get fresh copy of Owner team after creating repos. // Get fresh copy of Owner team after creating repos.
ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) ownerTeam, err = org.GetOwnerTeam()
assert.NoError(t, err, "GetOwnerTeam") assert.NoError(t, err, "GetOwnerTeam")
// Create teams and check repositories. // Create teams and check repositories.

View File

@ -57,7 +57,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
repoPath := repo_model.RepoPath(u.Name, opts.RepoName) repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
if u.IsOrganization() { if u.IsOrganization() {
t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) t, err := organization.OrgFromUser(u).GetOwnerTeam()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,7 +8,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"gitea.com/go-chi/binding" "gitea.com/go-chi/binding"
@ -18,14 +17,15 @@ import (
const ( const (
// ErrGitRefName is git reference name error // ErrGitRefName is git reference name error
ErrGitRefName = "GitRefNameError" ErrGitRefName = "GitRefNameError"
// ErrGlobPattern is returned when glob pattern is invalid // ErrGlobPattern is returned when glob pattern is invalid
ErrGlobPattern = "GlobPattern" ErrGlobPattern = "GlobPattern"
// ErrRegexPattern is returned when a regex pattern is invalid // ErrRegexPattern is returned when a regex pattern is invalid
ErrRegexPattern = "RegexPattern" ErrRegexPattern = "RegexPattern"
// ErrUsername is username error // ErrUsername is username error
ErrUsername = "UsernameError" ErrUsername = "UsernameError"
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
) )
// AddBindingRules adds additional binding rules // AddBindingRules adds additional binding rules
@ -37,7 +37,6 @@ func AddBindingRules() {
addRegexPatternRule() addRegexPatternRule()
addGlobOrRegexPatternRule() addGlobOrRegexPatternRule()
addUsernamePatternRule() addUsernamePatternRule()
addValidGroupTeamMapRule()
} }
func addGitRefNameBindingRule() { func addGitRefNameBindingRule() {
@ -168,23 +167,6 @@ func addUsernamePatternRule() {
}) })
} }
func addValidGroupTeamMapRule() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return strings.HasPrefix(rule, "ValidGroupTeamMap")
},
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
_, err := auth.UnmarshalGroupTeamMapping(fmt.Sprintf("%v", val))
if err != nil {
errs.Add([]string{name}, ErrInvalidGroupTeamMap, err.Error())
return false, errs
}
return true, errs
},
})
}
func portOnly(hostport string) string { func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':') colon := strings.IndexByte(hostport, ':')
if colon == -1 { if colon == -1 {

View File

@ -136,8 +136,6 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername: case validation.ErrUsername:
data["ErrorMsg"] = trName + l.Tr("form.username_error") data["ErrorMsg"] = trName + l.Tr("form.username_error")
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
default: default:
msg := errs[0].Classification msg := errs[0].Classification
if msg != "" && errs[0].Message != "" { if msg != "" && errs[0].Message != "" {

View File

@ -477,7 +477,6 @@ include_error = ` must contain substring '%s'.`
glob_pattern_error = ` glob pattern is invalid: %s.` glob_pattern_error = ` glob pattern is invalid: %s.`
regex_pattern_error = ` regex pattern is invalid: %s.` regex_pattern_error = ` regex pattern is invalid: %s.`
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
invalid_group_team_map_error = ` mapping is invalid: %s`
unknown_error = Unknown error: unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect. captcha_incorrect = The CAPTCHA code is incorrect.
password_not_match = The passwords do not match. password_not_match = The passwords do not match.
@ -2759,8 +2758,6 @@ auths.oauth2_required_claim_value_helper = Set this value to restrict login from
auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional) auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional)
auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above) auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above)
auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above) auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above)
auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above)
auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group.
auths.enable_auto_register = Enable Auto Registration auths.enable_auto_register = Enable Auto Registration
auths.sspi_auto_create_users = Automatically create users auths.sspi_auto_create_users = Automatically create users
auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time

View File

@ -507,7 +507,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
var err error var err error
if assignOrg { if assignOrg {
ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.Params(":org")) ctx.Org.Organization, err = organization.GetOrgByName(ctx.Params(":org"))
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org")) redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org"))
@ -687,7 +687,7 @@ func Routes(ctx gocontext.Context) *web.Route {
} }
// Get user from session if logged in. // Get user from session if logged in.
m.Use(auth.APIAuth(group)) m.Use(context.APIAuth(group))
m.Use(context.ToggleAPI(&context.ToggleOptions{ m.Use(context.ToggleAPI(&context.ToggleOptions{
SignInRequired: setting.Service.RequireSignInView, SignInRequired: setting.Service.RequireSignInView,

View File

@ -108,7 +108,7 @@ func CreateFork(ctx *context.APIContext) {
if form.Organization == nil { if form.Organization == nil {
forker = ctx.Doer forker = ctx.Doer
} else { } else {
org, err := organization.GetOrgByName(ctx, *form.Organization) org, err := organization.GetOrgByName(*form.Organization)
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err) ctx.Error(http.StatusUnprocessableEntity, "", err)

View File

@ -468,7 +468,7 @@ func CreateOrgRepo(ctx *context.APIContext) {
// "403": // "403":
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
opt := web.GetForm(ctx).(*api.CreateRepoOption) opt := web.GetForm(ctx).(*api.CreateRepoOption)
org, err := organization.GetOrgByName(ctx, ctx.Params(":org")) org, err := organization.GetOrgByName(ctx.Params(":org"))
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err) ctx.Error(http.StatusUnprocessableEntity, "", err)

View File

@ -204,8 +204,6 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
GroupClaimName: form.Oauth2GroupClaimName, GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup, RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup, AdminGroup: form.Oauth2AdminGroup,
GroupTeamMap: form.Oauth2GroupTeamMap,
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
} }
} }

View File

@ -16,7 +16,6 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
auth_service "code.gitea.io/gitea/services/auth" auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@ -268,11 +267,5 @@ func LinkAccountPostRegister(ctx *context.Context) {
return return
} }
source := authSource.Cfg.(*oauth2.Source)
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
handleSignIn(ctx, u, false) handleSignIn(ctx, u, false)
} }

View File

@ -17,9 +17,7 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
org_model "code.gitea.io/gitea/models/organization" org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -29,7 +27,6 @@ import (
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth" auth_service "code.gitea.io/gitea/services/auth"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@ -966,19 +963,12 @@ func SignInOAuthCallback(ctx *context.Context) {
IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm), IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm),
} }
source := authSource.Cfg.(*oauth2.Source) setUserGroupClaims(authSource, u, &gothUser)
setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser)
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled // error already handled
return return
} }
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
} else { } else {
// no existing user is found, request attach or new account // no existing user is found, request attach or new account
showLinkingLogin(ctx, gothUser) showLinkingLogin(ctx, gothUser)
@ -989,7 +979,7 @@ func SignInOAuthCallback(ctx *context.Context) {
handleOAuth2SignIn(ctx, authSource, u, gothUser) handleOAuth2SignIn(ctx, authSource, u, gothUser)
} }
func claimValueToStringSet(claimValue interface{}) container.Set[string] { func claimValueToStringSlice(claimValue interface{}) []string {
var groups []string var groups []string
switch rawGroup := claimValue.(type) { switch rawGroup := claimValue.(type) {
@ -1003,45 +993,37 @@ func claimValueToStringSet(claimValue interface{}) container.Set[string] {
str := fmt.Sprintf("%s", rawGroup) str := fmt.Sprintf("%s", rawGroup)
groups = strings.Split(str, ",") groups = strings.Split(str, ",")
} }
return container.SetOf(groups...) return groups
} }
func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error { func setUserGroupClaims(loginSource *auth.Source, u *user_model.User, gothUser *goth.User) bool {
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval { source := loginSource.Cfg.(*oauth2.Source)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap) if source.GroupClaimName == "" || (source.AdminGroup == "" && source.RestrictedGroup == "") {
if err != nil { return false
return err
}
groups := getClaimedGroups(source, gothUser)
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
return err
}
} }
return nil
}
func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[string] {
groupClaims, has := gothUser.RawData[source.GroupClaimName] groupClaims, has := gothUser.RawData[source.GroupClaimName]
if !has { if !has {
return nil return false
} }
return claimValueToStringSet(groupClaims) groups := claimValueToStringSlice(groupClaims)
}
func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool {
groups := getClaimedGroups(source, gothUser)
wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
if source.AdminGroup != "" { if source.AdminGroup != "" {
u.IsAdmin = groups.Contains(source.AdminGroup) u.IsAdmin = false
} }
if source.RestrictedGroup != "" { if source.RestrictedGroup != "" {
u.IsRestricted = groups.Contains(source.RestrictedGroup) u.IsRestricted = false
}
for _, g := range groups {
if source.AdminGroup != "" && g == source.AdminGroup {
u.IsAdmin = true
} else if source.RestrictedGroup != "" && g == source.RestrictedGroup {
u.IsRestricted = true
}
} }
return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted
@ -1088,15 +1070,6 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
needs2FA = err == nil needs2FA = err == nil
} }
oauth2Source := source.Cfg.(*oauth2.Source)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
if err != nil {
ctx.ServerError("UnmarshalGroupTeamMapping", err)
return
}
groups := getClaimedGroups(oauth2Source, &gothUser)
// If this user is enrolled in 2FA and this source doesn't override it, // If this user is enrolled in 2FA and this source doesn't override it,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page. // we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if !needs2FA { if !needs2FA {
@ -1115,7 +1088,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
u.SetLastLogin() u.SetLastLogin()
// Update GroupClaims // Update GroupClaims
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser) changed := setUserGroupClaims(source, u, &gothUser)
cols := []string{"last_login_unix"} cols := []string{"last_login_unix"}
if changed { if changed {
cols = append(cols, "is_admin", "is_restricted") cols = append(cols, "is_admin", "is_restricted")
@ -1126,13 +1099,6 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return return
} }
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
// update external user information // update external user information
if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil { if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) { if !errors.Is(err, util.ErrNotExist) {
@ -1155,7 +1121,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return return
} }
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser) changed := setUserGroupClaims(source, u, &gothUser)
if changed { if changed {
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil { if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil {
ctx.ServerError("UpdateUserCols", err) ctx.ServerError("UpdateUserCols", err)
@ -1163,13 +1129,6 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
} }
} }
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
if err := updateSession(ctx, nil, map[string]interface{}{ if err := updateSession(ctx, nil, map[string]interface{}{
// User needs to use 2FA, save data and redirect to 2FA page. // User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID, "twofaUid": u.ID,
@ -1229,9 +1188,15 @@ func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, res
} }
if oauth2Source.RequiredClaimValue != "" { if oauth2Source.RequiredClaimValue != "" {
groups := claimValueToStringSet(claimInterface) groups := claimValueToStringSlice(claimInterface)
found := false
if !groups.Contains(oauth2Source.RequiredClaimValue) { for _, group := range groups {
if group == oauth2Source.RequiredClaimValue {
found = true
break
}
}
if !found {
return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID} return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
} }
} }

View File

@ -78,7 +78,7 @@ func RetrieveLabels(ctx *context.Context) {
} }
ctx.Data["OrgLabels"] = orgLabels ctx.Data["OrgLabels"] = orgLabels
org, err := organization.GetOrgByName(ctx, ctx.Repo.Owner.LowerName) org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName)
if err != nil { if err != nil {
ctx.ServerError("GetOrgByName", err) ctx.ServerError("GetOrgByName", err)
return return

View File

@ -1006,7 +1006,7 @@ func AddTeamPost(ctx *context.Context) {
return return
} }
team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, name) team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(name)
if err != nil { if err != nil {
if organization.IsErrTeamNotExist(err) { if organization.IsErrTeamNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.team_not_exist")) ctx.Flash.Error(ctx.Tr("form.team_not_exist"))

View File

@ -203,7 +203,7 @@ func Routes(ctx gocontext.Context) *web.Route {
} }
// Get user from session if logged in. // Get user from session if logged in.
common = append(common, auth_service.Auth(group)) common = append(common, context.Auth(group))
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
common = append(common, middleware.GetHead) common = append(common, middleware.GetHead)

View File

@ -1,60 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web/middleware"
)
// Auth is a middleware to authenticate a web user
func Auth(authMethod Method) func(*context.Context) {
return func(ctx *context.Context) {
if err := authShared(ctx, authMethod); err != nil {
log.Error("Failed to verify user: %v", err)
ctx.Error(http.StatusUnauthorized, "Verify")
return
}
if ctx.Doer == nil {
// ensure the session uid is deleted
_ = ctx.Session.Delete("uid")
}
}
}
// APIAuth is a middleware to authenticate an api user
func APIAuth(authMethod Method) func(*context.APIContext) {
return func(ctx *context.APIContext) {
if err := authShared(ctx.Context, authMethod); err != nil {
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
}
}
}
func authShared(ctx *context.Context, authMethod Method) error {
var err error
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
return err
}
if ctx.Doer != nil {
if ctx.Locale.Language() != ctx.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName
ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data["SignedUser"] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["SignedUserName"] = ctx.Doer.Name
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
ctx.Data["SignedUserName"] = ""
}
return nil
}

View File

@ -10,10 +10,9 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/mailer" "code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user" user_service "code.gitea.io/gitea/services/user"
) )
@ -65,66 +64,61 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
} }
if user != nil { if user != nil {
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team)
source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
}
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) { if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) {
if err := asymkey_model.RewriteAllPublicKeys(); err != nil { return user, asymkey_model.RewriteAllPublicKeys()
return user, err
}
}
} else {
// Fallback.
if len(sr.Username) == 0 {
sr.Username = userName
}
if len(sr.Mail) == 0 {
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
}
user = &user_model.User{
LowerName: strings.ToLower(sr.Username),
Name: sr.Username,
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
Email: sr.Mail,
LoginType: source.authSource.Type,
LoginSource: source.authSource.ID,
LoginName: userName,
IsAdmin: sr.IsAdmin,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
IsActive: util.OptionalBoolTrue,
}
err := user_model.CreateUser(user, overwriteDefault)
if err != nil {
return user, err
}
mailer.SendRegisterNotifyMail(user)
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
return user, err
}
}
if len(source.AttributeAvatar) > 0 {
if err := user_service.UploadAvatar(user, sr.Avatar); err != nil {
return user, err
}
} }
return user, nil
} }
// Fallback.
if len(sr.Username) == 0 {
sr.Username = userName
}
if len(sr.Mail) == 0 {
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
}
user = &user_model.User{
LowerName: strings.ToLower(sr.Username),
Name: sr.Username,
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
Email: sr.Mail,
LoginType: source.authSource.Type,
LoginSource: source.authSource.ID,
LoginName: userName,
IsAdmin: sr.IsAdmin,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
IsActive: util.OptionalBoolTrue,
}
err := user_model.CreateUser(user, overwriteDefault)
if err != nil {
return user, err
}
mailer.SendRegisterNotifyMail(user)
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
err = asymkey_model.RewriteAllPublicKeys()
}
if err == nil && len(source.AttributeAvatar) > 0 {
_ = user_service.UploadAvatar(user, sr.Avatar)
}
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) { if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap) orgCache := make(map[string]*organization.Organization)
if err != nil { teamCache := make(map[string]*organization.Team)
return user, err source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
}
if err := source_service.SyncGroupsToTeams(db.DefaultContext, user, sr.Groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
return user, err
}
} }
return user, nil return user, err
} }
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication // IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication

View File

@ -0,0 +1,94 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package ldap
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
)
// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships
func (source *Source) SyncLdapGroupsToTeams(user *user_model.User, ldapTeamAdd, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) {
var err error
if source.GroupsEnabled && source.GroupTeamMapRemoval {
// when the user is not a member of configs LDAP group, remove mapped organizations/teams memberships
removeMappedMemberships(user, ldapTeamRemove, orgCache, teamCache)
}
for orgName, teamNames := range ldapTeamAdd {
org, ok := orgCache[orgName]
if !ok {
org, err = organization.GetOrgByName(orgName)
if err != nil {
// organization must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
continue
}
orgCache[orgName] = org
}
for _, teamName := range teamNames {
team, ok := teamCache[orgName+teamName]
if !ok {
team, err = org.GetTeam(teamName)
if err != nil {
// team must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
continue
}
teamCache[orgName+teamName] = team
}
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); !isMember && err == nil {
log.Trace("LDAP group sync: adding user [%s] to team [%s]", user.Name, org.Name)
} else {
continue
}
err := models.AddTeamMember(team, user.ID)
if err != nil {
log.Error("LDAP group sync: Could not add user to team: %v", err)
}
}
}
}
// remove membership to organizations/teams if user is not member of corresponding LDAP group
// e.g. lets assume user is member of LDAP group "x", but LDAP group team map contains LDAP groups "x" and "y"
// then users membership gets removed for all organizations/teams mapped by LDAP group "y"
func removeMappedMemberships(user *user_model.User, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) {
var err error
for orgName, teamNames := range ldapTeamRemove {
org, ok := orgCache[orgName]
if !ok {
org, err = organization.GetOrgByName(orgName)
if err != nil {
// organization must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
continue
}
orgCache[orgName] = org
}
for _, teamName := range teamNames {
team, ok := teamCache[orgName+teamName]
if !ok {
team, err = org.GetTeam(teamName)
if err != nil {
// team must must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
continue
}
}
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); isMember && err == nil {
log.Trace("LDAP group sync: removing user [%s] from team [%s]", user.Name, org.Name)
} else {
continue
}
err = models.RemoveTeamMember(team, user.ID)
if err != nil {
log.Error("LDAP group sync: Could not remove user from team: %v", err)
}
}
}
}

View File

@ -11,24 +11,26 @@ import (
"strconv" "strconv"
"strings" "strings"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
) )
// SearchResult : user data // SearchResult : user data
type SearchResult struct { type SearchResult struct {
Username string // Username Username string // Username
Name string // Name Name string // Name
Surname string // Surname Surname string // Surname
Mail string // E-mail address Mail string // E-mail address
SSHPublicKey []string // SSH Public Key SSHPublicKey []string // SSH Public Key
IsAdmin bool // if user is administrator IsAdmin bool // if user is administrator
IsRestricted bool // if user is restricted IsRestricted bool // if user is restricted
LowerName string // LowerName LowerName string // LowerName
Avatar []byte Avatar []byte
Groups container.Set[string] LdapTeamAdd map[string][]string // organizations teams to add
LdapTeamRemove map[string][]string // organizations teams to remove
} }
func (source *Source) sanitizedUserQuery(username string) (string, bool) { func (source *Source) sanitizedUserQuery(username string) (string, bool) {
@ -194,8 +196,9 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
} }
// List all group memberships of a user // List all group memberships of a user
func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) container.Set[string] { func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) []string {
ldapGroups := make(container.Set[string]) var ldapGroups []string
var searchFilter string
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter) groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
if !ok { if !ok {
@ -207,12 +210,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
return ldapGroups return ldapGroups
} }
var searchFilter string
if applyGroupFilter { if applyGroupFilter {
searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid)) searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid))
} else { } else {
searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid)) searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
} }
result, err := l.Search(ldap.NewSearchRequest( result, err := l.Search(ldap.NewSearchRequest(
groupDN, groupDN,
ldap.ScopeWholeSubtree, ldap.ScopeWholeSubtree,
@ -234,12 +237,44 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
log.Error("LDAP search was successful, but found no DN!") log.Error("LDAP search was successful, but found no DN!")
continue continue
} }
ldapGroups.Add(entry.DN) ldapGroups = append(ldapGroups, entry.DN)
} }
return ldapGroups return ldapGroups
} }
// parse LDAP groups and return map of ldap groups to organizations teams
func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
ldapGroupsToTeams := make(map[string]map[string][]string)
err := json.Unmarshal([]byte(source.GroupTeamMap), &ldapGroupsToTeams)
if err != nil {
log.Error("Failed to unmarshall LDAP teams map: %v", err)
return ldapGroupsToTeams
}
return ldapGroupsToTeams
}
// getMappedMemberships : returns the organizations and teams to modify the users membership
func (source *Source) getMappedMemberships(usersLdapGroups []string, uid string) (map[string][]string, map[string][]string) {
// unmarshall LDAP group team map from configs
ldapGroupsToTeams := source.mapLdapGroupsToTeams()
membershipsToAdd := map[string][]string{}
membershipsToRemove := map[string][]string{}
for group, memberships := range ldapGroupsToTeams {
isUserInGroup := util.SliceContainsString(usersLdapGroups, group)
if isUserInGroup {
for org, teams := range memberships {
membershipsToAdd[org] = teams
}
} else if !isUserInGroup {
for org, teams := range memberships {
membershipsToRemove[org] = teams
}
}
}
return membershipsToAdd, membershipsToRemove
}
func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string { func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
if strings.ToLower(source.UserUID) == "dn" { if strings.ToLower(source.UserUID) == "dn" {
return entry.DN return entry.DN
@ -364,6 +399,23 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname) surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(source.AttributeMail) mail := sr.Entries[0].GetAttributeValue(source.AttributeMail)
teamsToAdd := make(map[string][]string)
teamsToRemove := make(map[string][]string)
// Check group membership
if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
return nil
}
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
}
}
if isAttributeSSHPublicKeySet { if isAttributeSSHPublicKeySet {
sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey) sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
} }
@ -379,17 +431,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar) Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
} }
// Check group membership
var usersLdapGroups container.Set[string]
if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
return nil
}
}
if !directBind && source.AttributesInBind { if !directBind && source.AttributesInBind {
// binds user (checking password) after looking-up attributes in BindDN context // binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd) err = bindUser(l, userDN, passwd)
@ -399,16 +440,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
} }
return &SearchResult{ return &SearchResult{
LowerName: strings.ToLower(username), LowerName: strings.ToLower(username),
Username: username, Username: username,
Name: firstname, Name: firstname,
Surname: surname, Surname: surname,
Mail: mail, Mail: mail,
SSHPublicKey: sshPublicKey, SSHPublicKey: sshPublicKey,
IsAdmin: isAdmin, IsAdmin: isAdmin,
IsRestricted: isRestricted, IsRestricted: isRestricted,
Avatar: Avatar, Avatar: Avatar,
Groups: usersLdapGroups, LdapTeamAdd: teamsToAdd,
LdapTeamRemove: teamsToRemove,
} }
} }
@ -470,29 +512,33 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
result := make([]*SearchResult, 0, len(sr.Entries)) result := make([]*SearchResult, 0, len(sr.Entries))
for _, v := range sr.Entries { for _, v := range sr.Entries {
var usersLdapGroups container.Set[string] teamsToAdd := make(map[string][]string)
teamsToRemove := make(map[string][]string)
if source.GroupsEnabled { if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(v) userAttributeListedInGroup := source.getUserAttributeListedInGroup(v)
if source.GroupFilter != "" { if source.GroupFilter != "" {
usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true) usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if len(usersLdapGroups) == 0 { if len(usersLdapGroups) == 0 {
continue continue
} }
} }
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval { if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, false) usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
} }
} }
user := &SearchResult{ user := &SearchResult{
Username: v.GetAttributeValue(source.AttributeUsername), Username: v.GetAttributeValue(source.AttributeUsername),
Name: v.GetAttributeValue(source.AttributeName), Name: v.GetAttributeValue(source.AttributeName),
Surname: v.GetAttributeValue(source.AttributeSurname), Surname: v.GetAttributeValue(source.AttributeSurname),
Mail: v.GetAttributeValue(source.AttributeMail), Mail: v.GetAttributeValue(source.AttributeMail),
IsAdmin: checkAdmin(l, source, v.DN), IsAdmin: checkAdmin(l, source, v.DN),
Groups: usersLdapGroups, LdapTeamAdd: teamsToAdd,
LdapTeamRemove: teamsToRemove,
} }
if !user.IsAdmin { if !user.IsAdmin {

View File

@ -13,10 +13,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user" user_service "code.gitea.io/gitea/services/user"
) )
@ -67,11 +65,6 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
orgCache := make(map[string]*organization.Organization) orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team) teamCache := make(map[string]*organization.Team)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
if err != nil {
return err
}
for _, su := range sr { for _, su := range sr {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -180,9 +173,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
} }
// Synchronize LDAP groups with organization and team memberships // Synchronize LDAP groups with organization and team memberships
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) { if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
if err := source_service.SyncGroupsToTeamsCached(ctx, usr, su.Groups, groupTeamMapping, source.GroupTeamMapRemoval, orgCache, teamCache); err != nil { source.SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, orgCache, teamCache)
log.Error("SyncGroupsToTeamsCached: %v", err)
}
} }
} }

View File

@ -8,6 +8,13 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
) )
// ________ _____ __ .__ ________
// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
// / | \ / /_\ \| | \ __\ | \ / ____/
// / | \/ | \ | /| | | Y \/ \
// \_______ /\____|__ /____/ |__| |___| /\_______ \
// \/ \/ \/ \/
// Source holds configuration for the OAuth2 login source. // Source holds configuration for the OAuth2 login source.
type Source struct { type Source struct {
Provider string Provider string
@ -17,15 +24,13 @@ type Source struct {
CustomURLMapping *CustomURLMapping CustomURLMapping *CustomURLMapping
IconURL string IconURL string
Scopes []string Scopes []string
RequiredClaimName string RequiredClaimName string
RequiredClaimValue string RequiredClaimValue string
GroupClaimName string GroupClaimName string
AdminGroup string AdminGroup string
GroupTeamMap string RestrictedGroup string
GroupTeamMapRemoval bool SkipLocalTwoFA bool `json:",omitempty"`
RestrictedGroup string
SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource // reference to the authSource
authSource *auth.Source authSource *auth.Source

View File

@ -1,116 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package source
import (
"context"
"fmt"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
)
type syncType int
const (
syncAdd syncType = iota
syncRemove
)
// SyncGroupsToTeams maps authentication source groups to organization and team memberships
func SyncGroupsToTeams(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool) error {
orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team)
return SyncGroupsToTeamsCached(ctx, user, sourceUserGroups, sourceGroupTeamMapping, performRemoval, orgCache, teamCache)
}
// SyncGroupsToTeamsCached maps authentication source groups to organization and team memberships
func SyncGroupsToTeamsCached(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
membershipsToAdd, membershipsToRemove := resolveMappedMemberships(sourceUserGroups, sourceGroupTeamMapping)
if performRemoval {
if err := syncGroupsToTeamsCached(ctx, user, membershipsToRemove, syncRemove, orgCache, teamCache); err != nil {
return fmt.Errorf("could not sync[remove] user groups: %w", err)
}
}
if err := syncGroupsToTeamsCached(ctx, user, membershipsToAdd, syncAdd, orgCache, teamCache); err != nil {
return fmt.Errorf("could not sync[add] user groups: %w", err)
}
return nil
}
func resolveMappedMemberships(sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string) (map[string][]string, map[string][]string) {
membershipsToAdd := map[string][]string{}
membershipsToRemove := map[string][]string{}
for group, memberships := range sourceGroupTeamMapping {
isUserInGroup := sourceUserGroups.Contains(group)
if isUserInGroup {
for org, teams := range memberships {
membershipsToAdd[org] = teams
}
} else {
for org, teams := range memberships {
membershipsToRemove[org] = teams
}
}
}
return membershipsToAdd, membershipsToRemove
}
func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeamMap map[string][]string, action syncType, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
for orgName, teamNames := range orgTeamMap {
var err error
org, ok := orgCache[orgName]
if !ok {
org, err = organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
// organization must be created before group sync
log.Warn("group sync: Could not find organisation %s: %v", orgName, err)
continue
}
return err
}
orgCache[orgName] = org
}
for _, teamName := range teamNames {
team, ok := teamCache[orgName+teamName]
if !ok {
team, err = org.GetTeam(ctx, teamName)
if err != nil {
if organization.IsErrTeamNotExist(err) {
// team must be created before group sync
log.Warn("group sync: Could not find team %s: %v", teamName, err)
continue
}
return err
}
teamCache[orgName+teamName] = team
}
isMember, err := organization.IsTeamMember(ctx, org.ID, team.ID, user.ID)
if err != nil {
return err
}
if action == syncAdd && !isMember {
if err := models.AddTeamMember(team, user.ID); err != nil {
log.Error("group sync: Could not add user to team: %v", err)
return err
}
} else if action == syncRemove && isMember {
if err := models.RemoveTeamMember(team, user.ID); err != nil {
log.Error("group sync: Could not remove user from team: %v", err)
return err
}
}
}
}
return nil
}

View File

@ -72,15 +72,13 @@ type AuthenticationForm struct {
Oauth2GroupClaimName string Oauth2GroupClaimName string
Oauth2AdminGroup string Oauth2AdminGroup string
Oauth2RestrictedGroup string Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
SkipLocalTwoFA bool SkipLocalTwoFA bool
SSPIAutoCreateUsers bool SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool SSPIAutoActivateUsers bool
SSPIStripDomainNames bool SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"` SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string SSPIDefaultLanguage string
GroupTeamMap string `binding:"ValidGroupTeamMap"` GroupTeamMap string
GroupTeamMapRemoval bool GroupTeamMapRemoval bool
} }

View File

@ -98,9 +98,6 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr
} }
for _, ref := range refs { for _, ref := range refs {
if ref.RefAction == references.XRefActionCloses { if ref.RefAction == references.XRefActionCloses {
if err := ref.LoadIssue(ctx); err != nil {
return "", "", err
}
closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index)) closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index))
} }
} }

View File

@ -361,14 +361,6 @@
<label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label> <label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label>
<input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{$cfg.RestrictedGroup}}"> <input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{$cfg.RestrictedGroup}}">
</div> </div>
<div class="field">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team"}}</label>
<input name="oauth2_group_team_map" value="{{$cfg.GroupTeamMap}}" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team_removal"}}</label>
<input name="oauth2_group_team_map_removal" type="checkbox" {{if $cfg.GroupTeamMapRemoval}}checked{{end}}>
</div>
{{end}} {{end}}
<!-- SSPI --> <!-- SSPI -->

View File

@ -52,7 +52,7 @@
</div> </div>
<div class="field"> <div class="field">
<label for="restricted_filter">{{.locale.Tr "admin.auths.restricted_filter"}}</label> <label for="restricted_filter">{{.locale.Tr "admin.auths.restricted_filter"}}</label>
<input id="restricted_filter" name="restricted_filter" value="{{.restricted_filter}}"> <input id="restricted_filter" name="admin_filter" value="{{.restricted_filter}}">
<p class="help">{{.locale.Tr "admin.auths.restricted_filter_helper"}}</p> <p class="help">{{.locale.Tr "admin.auths.restricted_filter_helper"}}</p>
</div> </div>
<div class="field"> <div class="field">

View File

@ -98,12 +98,4 @@
<label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label> <label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label>
<input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{.oauth2_group_claim_name}}"> <input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{.oauth2_group_claim_name}}">
</div> </div>
<div class="field">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team"}}</label>
<input name="oauth2_group_team_map" value="{{.group_team_map}}" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team_removal"}}</label>
<input name="oauth2_group_team_map_removal" type="checkbox" {{if .group_team_map_removal}}checked{{end}}>
</div>
</div> </div>

View File

@ -112,14 +112,23 @@ func getLDAPServerPort() string {
return port return port
} }
func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string { func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) {
groupTeamMapRemoval := "off"
groupTeamMap := ""
if len(groupMapParams) == 2 {
groupTeamMapRemoval = groupMapParams[0]
groupTeamMap = groupMapParams[1]
}
// Modify user filter to test group filter explicitly // Modify user filter to test group filter explicitly
userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))" userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))"
if groupFilter != "" { if groupFilter != "" {
userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))" userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))"
} }
return map[string]string{ session := loginUser(t, "user1")
csrf := GetCSRF(t, session, "/admin/auths/new")
req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{
"_csrf": csrf, "_csrf": csrf,
"type": "2", "type": "2",
"name": "ldap", "name": "ldap",
@ -145,19 +154,7 @@ func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap
"group_team_map": groupTeamMap, "group_team_map": groupTeamMap,
"group_team_map_removal": groupTeamMapRemoval, "group_team_map_removal": groupTeamMapRemoval,
"user_uid": "DN", "user_uid": "DN",
} })
}
func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) {
groupTeamMapRemoval := "off"
groupTeamMap := ""
if len(groupMapParams) == 2 {
groupTeamMapRemoval = groupMapParams[0]
groupTeamMap = groupMapParams[1]
}
session := loginUser(t, "user1")
csrf := GetCSRF(t, session, "/admin/auths/new")
req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval))
session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusSeeOther)
} }
@ -205,7 +202,26 @@ func TestLDAPAuthChange(t *testing.T) {
binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value") binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com") assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com")
req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off")) req = NewRequestWithValues(t, "POST", href, map[string]string{
"_csrf": csrf,
"type": "2",
"name": "ldap",
"host": getLDAPServerHost(),
"port": "389",
"bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com",
"bind_password": "password",
"user_base": "ou=people,dc=planetexpress,dc=com",
"filter": "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))",
"admin_filter": "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
"restricted_filter": "(uid=leela)",
"attribute_username": "uid",
"attribute_name": "givenName",
"attribute_surname": "sn",
"attribute_mail": "mail",
"attribute_ssh_public_key": "",
"is_sync_enabled": "on",
"is_active": "on",
})
session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequest(t, "GET", href) req = NewRequest(t, "GET", href)
@ -379,7 +395,7 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
} }
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`) addAuthSourceLDAP(t, "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`)
org, err := organization.GetOrgByName(db.DefaultContext, "org26") org, err := organization.GetOrgByName("org26")
assert.NoError(t, err) assert.NoError(t, err)
team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
assert.NoError(t, err) assert.NoError(t, err)
@ -424,7 +440,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
} }
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`) addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
org, err := organization.GetOrgByName(db.DefaultContext, "org26") org, err := organization.GetOrgByName("org26")
assert.NoError(t, err) assert.NoError(t, err)
team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
assert.NoError(t, err) assert.NoError(t, err)
@ -452,15 +468,24 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
assert.False(t, isMember, "User membership should have been removed from team") assert.False(t, isMember, "User membership should have been removed from team")
} }
func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) { // Login should work even if Team Group Map contains a broken JSON
func TestBrokenLDAPMapUserSignin(t *testing.T) {
if skipLDAPTests() { if skipLDAPTests() {
t.Skip() t.Skip()
return return
} }
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "on", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`)
session := loginUser(t, "user1") u := gitLDAPUsers[0]
csrf := GetCSRF(t, session, "/admin/auths/new")
req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off")) session := loginUserWithPassword(t, u.UserName, u.Password)
session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok req := NewRequest(t, "GET", "/user/settings")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
} }

View File

@ -20,8 +20,7 @@
<SvgIcon name="octicon-meter" class="ui text yellow" class-name="job-status-rotate" v-else-if="job.status === 'running'"/> <SvgIcon name="octicon-meter" class="ui text yellow" class-name="job-status-rotate" v-else-if="job.status === 'running'"/>
<SvgIcon name="octicon-x-circle-fill" class="red" v-else/> <SvgIcon name="octicon-x-circle-fill" class="red" v-else/>
{{ job.name }} {{ job.name }}
<!-- TODO: it will be a better idea to move "button" out from "a", and the ".prevent" will not be needed. But it needs some work with CSS --> <button class="job-brief-rerun" @click="rerunJob(index)" v-if="job.canRerun">
<button class="job-brief-rerun" @click.prevent="rerunJob(index)" v-if="job.canRerun">
<SvgIcon name="octicon-sync" class="ui text black"/> <SvgIcon name="octicon-sync" class="ui text black"/>
</button> </button>
</a> </a>
@ -163,14 +162,12 @@ const sfc = {
} }
}, },
// rerun a job // rerun a job
async rerunJob(idx) { rerunJob(idx) {
const jobLink = `${this.run.link}/jobs/${idx}`; this.fetch(`${this.run.link}/jobs/${idx}/rerun`);
await this.fetchPost(`${jobLink}/rerun`);
window.location.href = jobLink;
}, },
// cancel a run // cancel a run
cancelRun() { cancelRun() {
this.fetchPost(`${this.run.link}/cancel`); this.fetch(`${this.run.link}/cancel`);
}, },
createLogLine(line) { createLogLine(line) {
@ -208,7 +205,7 @@ const sfc = {
// for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc // for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
return {step: idx, cursor: it.cursor, expanded: it.expanded}; return {step: idx, cursor: it.cursor, expanded: it.expanded};
}); });
const resp = await this.fetchPost( const resp = await this.fetch(
`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, `${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`,
JSON.stringify({logCursors}), JSON.stringify({logCursors}),
); );
@ -248,7 +245,7 @@ const sfc = {
} }
}, },
fetchPost(url, body) { fetch(url, body) {
return fetch(url, { return fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {