aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2023-02-08 07:44:42 +0100
committerGitHub <noreply@github.com>2023-02-08 14:44:42 +0800
commite8186f1c0f194ce3f63bed9a564002b80c0859c9 (patch)
tree75ffc50f54af2ef441ecf60448531b9e0ed64490
parent2c6cc0b8c982b3d49a5b208f75e15b2269584312 (diff)
downloadgitea-e8186f1c0f194ce3f63bed9a564002b80c0859c9.tar.gz
gitea-e8186f1c0f194ce3f63bed9a564002b80c0859c9.zip
Map OIDC groups to Orgs/Teams (#21441)
Fixes #19555 Test-Instructions: https://github.com/go-gitea/gitea/pull/21441#issuecomment-1419438000 This PR implements the mapping of user groups provided by OIDC providers to orgs teams in Gitea. The main part is a refactoring of the existing LDAP code to make it usable from different providers. Refactorings: - Moved the router auth code from module to service because of import cycles - Changed some model methods to take a `Context` parameter - Moved the mapping code from LDAP to a common location I've tested it with Keycloak but other providers should work too. The JSON mapping format is the same as for LDAP. ![grafik](https://user-images.githubusercontent.com/1666336/195634392-3fc540fc-b229-4649-99ac-91ae8e19df2d.png) --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
-rw-r--r--cmd/admin.go17
-rw-r--r--docs/content/doc/usage/command-line.en-us.md2
-rw-r--r--models/organization/org.go20
-rw-r--r--models/organization/org_test.go16
-rw-r--r--modules/auth/common.go22
-rw-r--r--modules/context/api.go30
-rw-r--r--modules/context/context.go32
-rw-r--r--modules/context/org.go2
-rw-r--r--modules/repository/create_test.go4
-rw-r--r--modules/repository/repo.go2
-rw-r--r--modules/validation/binding.go24
-rw-r--r--modules/web/middleware/binding.go2
-rw-r--r--options/locale/locale_en-US.ini3
-rw-r--r--routers/api/v1/api.go4
-rw-r--r--routers/api/v1/repo/fork.go2
-rw-r--r--routers/api/v1/repo/repo.go2
-rw-r--r--routers/web/admin/auths.go2
-rw-r--r--routers/web/auth/linkaccount.go7
-rw-r--r--routers/web/auth/oauth.go95
-rw-r--r--routers/web/repo/issue_label.go2
-rw-r--r--routers/web/repo/setting.go2
-rw-r--r--routers/web/web.go2
-rw-r--r--services/auth/middleware.go60
-rw-r--r--services/auth/source/ldap/source_authenticate.go96
-rw-r--r--services/auth/source/ldap/source_group_sync.go94
-rw-r--r--services/auth/source/ldap/source_search.go136
-rw-r--r--services/auth/source/ldap/source_sync.go11
-rw-r--r--services/auth/source/oauth2/source.go23
-rw-r--r--services/auth/source/source_group_sync.go116
-rw-r--r--services/forms/auth_form.go4
-rw-r--r--templates/admin/auth/edit.tmpl8
-rw-r--r--templates/admin/auth/source/ldap.tmpl2
-rw-r--r--templates/admin/auth/source/oauth.tmpl8
-rw-r--r--tests/integration/auth_ldap_test.go71
34 files changed, 500 insertions, 423 deletions
diff --git a/cmd/admin.go b/cmd/admin.go
index 4e2dc2bf06..318c212d08 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -372,6 +372,15 @@ var (
Value: "",
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{
@@ -853,6 +862,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
GroupClaimName: c.String("group-claim-name"),
AdminGroup: c.String("admin-group"),
RestrictedGroup: c.String("restricted-group"),
+ GroupTeamMap: c.String("group-team-map"),
+ GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
}
}
@@ -935,6 +946,12 @@ func runUpdateOauth(c *cli.Context) error {
if c.IsSet("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
customURLMapping := &oauth2.CustomURLMapping{}
diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md
index f2e72d4fc0..9b861a9da3 100644
--- a/docs/content/doc/usage/command-line.en-us.md
+++ b/docs/content/doc/usage/command-line.en-us.md
@@ -137,6 +137,8 @@ Admin operations:
- `--group-claim-name`: Claim name providing group names for this source. (Optional)
- `--admin-group`: Group Claim value for administrator 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:
- `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE`
- `update-oauth`:
diff --git a/models/organization/org.go b/models/organization/org.go
index 05eaead60b..852facf704 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -110,22 +110,14 @@ func (org *Organization) CanCreateOrgRepo(uid int64) (bool, error) {
return CanCreateOrgRepo(db.DefaultContext, org.ID, uid)
}
-func (org *Organization) getTeam(ctx context.Context, name string) (*Team, error) {
- 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)
+func (org *Organization) GetTeam(ctx context.Context, name string) (*Team, error) {
+ return GetTeam(ctx, org.ID, name)
}
// GetOwnerTeam returns owner team of organization.
-func (org *Organization) GetOwnerTeam() (*Team, error) {
- return org.getOwnerTeam(db.DefaultContext)
+func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) {
+ return org.GetTeam(ctx, OwnerTeamName)
}
// FindOrgTeams returns all teams of a given organization
@@ -342,7 +334,7 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) {
}
// GetOrgByName returns organization by given name.
-func GetOrgByName(name string) (*Organization, error) {
+func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
if len(name) == 0 {
return nil, ErrOrgNotExist{0, name}
}
@@ -350,7 +342,7 @@ func GetOrgByName(name string) (*Organization, error) {
LowerName: strings.ToLower(name),
Type: user_model.UserTypeOrganization,
}
- has, err := db.GetEngine(db.DefaultContext).Get(u)
+ has, err := db.GetEngine(ctx).Get(u)
if err != nil {
return nil, err
} else if !has {
diff --git a/models/organization/org_test.go b/models/organization/org_test.go
index 0a38365924..cfa304d7b2 100644
--- a/models/organization/org_test.go
+++ b/models/organization/org_test.go
@@ -61,28 +61,28 @@ func TestUser_IsOrgMember(t *testing.T) {
func TestUser_GetTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
- team, err := org.GetTeam("team1")
+ team, err := org.GetTeam(db.DefaultContext, "team1")
assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID)
assert.Equal(t, "team1", team.LowerName)
- _, err = org.GetTeam("does not exist")
+ _, err = org.GetTeam(db.DefaultContext, "does not exist")
assert.True(t, organization.IsErrTeamNotExist(err))
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
- _, err = nonOrg.GetTeam("team")
+ _, err = nonOrg.GetTeam(db.DefaultContext, "team")
assert.True(t, organization.IsErrTeamNotExist(err))
}
func TestUser_GetOwnerTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
- team, err := org.GetOwnerTeam()
+ team, err := org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID)
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
- _, err = nonOrg.GetOwnerTeam()
+ _, err = nonOrg.GetOwnerTeam(db.DefaultContext)
assert.True(t, organization.IsErrTeamNotExist(err))
}
@@ -115,15 +115,15 @@ func TestUser_GetMembers(t *testing.T) {
func TestGetOrgByName(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- org, err := organization.GetOrgByName("user3")
+ org, err := organization.GetOrgByName(db.DefaultContext, "user3")
assert.NoError(t, err)
assert.EqualValues(t, 3, org.ID)
assert.Equal(t, "user3", org.Name)
- _, err = organization.GetOrgByName("user2") // user2 is an individual
+ _, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual
assert.True(t, organization.IsErrOrgNotExist(err))
- _, err = organization.GetOrgByName("") // corner case
+ _, err = organization.GetOrgByName(db.DefaultContext, "") // corner case
assert.True(t, organization.IsErrOrgNotExist(err))
}
diff --git a/modules/auth/common.go b/modules/auth/common.go
new file mode 100644
index 0000000000..77361f6561
--- /dev/null
+++ b/modules/auth/common.go
@@ -0,0 +1,22 @@
+// 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
+}
diff --git a/modules/context/api.go b/modules/context/api.go
index 3f52c54d4c..3f938948ae 100644
--- a/modules/context/api.go
+++ b/modules/context/api.go
@@ -19,7 +19,6 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
- auth_service "code.gitea.io/gitea/services/auth"
)
// APIContext is a specific context for API service
@@ -215,35 +214,6 @@ 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
func APIContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
diff --git a/modules/context/context.go b/modules/context/context.go
index 84f40ce063..a2088217ff 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -36,7 +36,6 @@ import (
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/auth"
"gitea.com/go-chi/cache"
"gitea.com/go-chi/session"
@@ -659,37 +658,6 @@ 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.
func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
_, rnd := templates.HTMLRenderer(ctx)
diff --git a/modules/context/org.go b/modules/context/org.go
index ff3a5ae7ec..0add7f2c0c 100644
--- a/modules/context/org.go
+++ b/modules/context/org.go
@@ -80,7 +80,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
orgName := ctx.Params(":org")
var err error
- ctx.Org.Organization, err = organization.GetOrgByName(orgName)
+ ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(orgName)
diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go
index 293071bdce..e620422bcb 100644
--- a/modules/repository/create_test.go
+++ b/modules/repository/create_test.go
@@ -49,7 +49,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization")
// Check Owner team.
- ownerTeam, err := org.GetOwnerTeam()
+ ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err, "GetOwnerTeam")
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.
- ownerTeam, err = org.GetOwnerTeam()
+ ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err, "GetOwnerTeam")
// Create teams and check repositories.
diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index d1a70e7c15..c03e469990 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -57,7 +57,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
if u.IsOrganization() {
- t, err := organization.OrgFromUser(u).GetOwnerTeam()
+ t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
if err != nil {
return nil, err
}
diff --git a/modules/validation/binding.go b/modules/validation/binding.go
index ef0d01e80f..1f904979ff 100644
--- a/modules/validation/binding.go
+++ b/modules/validation/binding.go
@@ -8,6 +8,7 @@ import (
"regexp"
"strings"
+ "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/git"
"gitea.com/go-chi/binding"
@@ -17,15 +18,14 @@ import (
const (
// ErrGitRefName is git reference name error
ErrGitRefName = "GitRefNameError"
-
// ErrGlobPattern is returned when glob pattern is invalid
ErrGlobPattern = "GlobPattern"
-
// ErrRegexPattern is returned when a regex pattern is invalid
ErrRegexPattern = "RegexPattern"
-
// ErrUsername is username error
ErrUsername = "UsernameError"
+ // ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
+ ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
)
// AddBindingRules adds additional binding rules
@@ -37,6 +37,7 @@ func AddBindingRules() {
addRegexPatternRule()
addGlobOrRegexPatternRule()
addUsernamePatternRule()
+ addValidGroupTeamMapRule()
}
func addGitRefNameBindingRule() {
@@ -167,6 +168,23 @@ 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 {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go
index 733f00a1d5..8b74a864d9 100644
--- a/modules/web/middleware/binding.go
+++ b/modules/web/middleware/binding.go
@@ -136,6 +136,8 @@ 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)
case validation.ErrUsername:
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:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a7506986f6..f784b10c8d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -477,6 +477,7 @@ include_error = ` must contain substring '%s'.`
glob_pattern_error = ` glob 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.`
+invalid_group_team_map_error = ` mapping is invalid: %s`
unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect.
password_not_match = The passwords do not match.
@@ -2758,6 +2759,8 @@ 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_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_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.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
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 5f57977c29..1d2f8b18e0 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -507,7 +507,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
var err error
if assignOrg {
- ctx.Org.Organization, err = organization.GetOrgByName(ctx.Params(":org"))
+ ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.Params(":org"))
if err != nil {
if organization.IsErrOrgNotExist(err) {
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.
- m.Use(context.APIAuth(group))
+ m.Use(auth.APIAuth(group))
m.Use(context.ToggleAPI(&context.ToggleOptions{
SignInRequired: setting.Service.RequireSignInView,
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index e4c7eb7041..5b564a8066 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -108,7 +108,7 @@ func CreateFork(ctx *context.APIContext) {
if form.Organization == nil {
forker = ctx.Doer
} else {
- org, err := organization.GetOrgByName(*form.Organization)
+ org, err := organization.GetOrgByName(ctx, *form.Organization)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index e0cae5f82c..1426d1dbcc 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -468,7 +468,7 @@ func CreateOrgRepo(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
opt := web.GetForm(ctx).(*api.CreateRepoOption)
- org, err := organization.GetOrgByName(ctx.Params(":org"))
+ org, err := organization.GetOrgByName(ctx, ctx.Params(":org"))
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 1bc166902c..8ce45720fe 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -204,6 +204,8 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup,
+ GroupTeamMap: form.Oauth2GroupTeamMap,
+ GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
}
}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 6c409c6b9d..47a0daa06d 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
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/forms"
@@ -267,5 +268,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
return
}
+ source := authSource.Cfg.(*oauth2.Source)
+ if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
+ ctx.ServerError("SyncGroupsToTeams", err)
+ return
+ }
+
handleSignIn(ctx, u, false)
}
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index be60a0c73b..a11417da16 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -17,7 +17,9 @@ import (
"code.gitea.io/gitea/models/auth"
org_model "code.gitea.io/gitea/models/organization"
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/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
@@ -27,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
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/externalaccount"
"code.gitea.io/gitea/services/forms"
@@ -963,12 +966,19 @@ func SignInOAuthCallback(ctx *context.Context) {
IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm),
}
- setUserGroupClaims(authSource, u, &gothUser)
+ source := authSource.Cfg.(*oauth2.Source)
+
+ setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser)
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
return
}
+
+ if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
+ ctx.ServerError("SyncGroupsToTeams", err)
+ return
+ }
} else {
// no existing user is found, request attach or new account
showLinkingLogin(ctx, gothUser)
@@ -979,7 +989,7 @@ func SignInOAuthCallback(ctx *context.Context) {
handleOAuth2SignIn(ctx, authSource, u, gothUser)
}
-func claimValueToStringSlice(claimValue interface{}) []string {
+func claimValueToStringSet(claimValue interface{}) container.Set[string] {
var groups []string
switch rawGroup := claimValue.(type) {
@@ -993,37 +1003,45 @@ func claimValueToStringSlice(claimValue interface{}) []string {
str := fmt.Sprintf("%s", rawGroup)
groups = strings.Split(str, ",")
}
- return groups
+ return container.SetOf(groups...)
}
-func setUserGroupClaims(loginSource *auth.Source, u *user_model.User, gothUser *goth.User) bool {
- source := loginSource.Cfg.(*oauth2.Source)
- if source.GroupClaimName == "" || (source.AdminGroup == "" && source.RestrictedGroup == "") {
- return false
+func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error {
+ if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
+ groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
+ if err != nil {
+ 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]
if !has {
- return false
+ return nil
}
- groups := claimValueToStringSlice(groupClaims)
+ return claimValueToStringSet(groupClaims)
+}
+
+func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool {
+ groups := getClaimedGroups(source, gothUser)
wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
if source.AdminGroup != "" {
- u.IsAdmin = false
+ u.IsAdmin = groups.Contains(source.AdminGroup)
}
if 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
- }
+ u.IsRestricted = groups.Contains(source.RestrictedGroup)
}
return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted
@@ -1070,6 +1088,15 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
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,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if !needs2FA {
@@ -1088,7 +1115,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
u.SetLastLogin()
// Update GroupClaims
- changed := setUserGroupClaims(source, u, &gothUser)
+ changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
cols := []string{"last_login_unix"}
if changed {
cols = append(cols, "is_admin", "is_restricted")
@@ -1099,6 +1126,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
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
if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) {
@@ -1121,7 +1155,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return
}
- changed := setUserGroupClaims(source, u, &gothUser)
+ changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
if changed {
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil {
ctx.ServerError("UpdateUserCols", err)
@@ -1129,6 +1163,13 @@ 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{}{
// User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID,
@@ -1188,15 +1229,9 @@ func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, res
}
if oauth2Source.RequiredClaimValue != "" {
- groups := claimValueToStringSlice(claimInterface)
- found := false
- for _, group := range groups {
- if group == oauth2Source.RequiredClaimValue {
- found = true
- break
- }
- }
- if !found {
+ groups := claimValueToStringSet(claimInterface)
+
+ if !groups.Contains(oauth2Source.RequiredClaimValue) {
return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
}
}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 01421dc927..66e8920bd9 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -78,7 +78,7 @@ func RetrieveLabels(ctx *context.Context) {
}
ctx.Data["OrgLabels"] = orgLabels
- org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName)
+ org, err := organization.GetOrgByName(ctx, ctx.Repo.Owner.LowerName)
if err != nil {
ctx.ServerError("GetOrgByName", err)
return
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index 2cc263e5bb..5c30795f22 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -1006,7 +1006,7 @@ func AddTeamPost(ctx *context.Context) {
return
}
- team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(name)
+ team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, name)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
diff --git a/routers/web/web.go b/routers/web/web.go
index 6898956053..88e27ad678 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -203,7 +203,7 @@ func Routes(ctx gocontext.Context) *web.Route {
}
// Get user from session if logged in.
- common = append(common, context.Auth(group))
+ common = append(common, auth_service.Auth(group))
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
common = append(common, middleware.GetHead)
diff --git a/services/auth/middleware.go b/services/auth/middleware.go
new file mode 100644
index 0000000000..cccaab2998
--- /dev/null
+++ b/services/auth/middleware.go
@@ -0,0 +1,60 @@
+// 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
+}
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 321cf5540d..fba8da7934 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -10,9 +10,10 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
+ auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/util"
+ source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
)
@@ -64,61 +65,66 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
}
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) {
- return user, asymkey_model.RewriteAllPublicKeys()
+ if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
+ return user, err
+ }
+ }
+ } else {
+ // Fallback.
+ if len(sr.Username) == 0 {
+ sr.Username = userName
}
- 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)
- }
+ 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,
- }
+ 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
- }
+ err := user_model.CreateUser(user, overwriteDefault)
+ if err != nil {
+ return user, err
+ }
- mailer.SendRegisterNotifyMail(user)
+ 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 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
+ }
+ }
}
+
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)
+ groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
+ if err != nil {
+ return user, err
+ }
+ if err := source_service.SyncGroupsToTeams(db.DefaultContext, user, sr.Groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
+ return user, err
+ }
}
- return user, err
+ return user, nil
}
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
diff --git a/services/auth/source/ldap/source_group_sync.go b/services/auth/source/ldap/source_group_sync.go
deleted file mode 100644
index 95a6084922..0000000000
--- a/services/auth/source/ldap/source_group_sync.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// 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)
- }
- }
- }
-}
diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index 16f13029f9..5a2d25b0c4 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -11,26 +11,24 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
"github.com/go-ldap/ldap/v3"
)
// SearchResult : user data
type SearchResult struct {
- Username string // Username
- Name string // Name
- Surname string // Surname
- Mail string // E-mail address
- SSHPublicKey []string // SSH Public Key
- IsAdmin bool // if user is administrator
- IsRestricted bool // if user is restricted
- LowerName string // LowerName
- Avatar []byte
- LdapTeamAdd map[string][]string // organizations teams to add
- LdapTeamRemove map[string][]string // organizations teams to remove
+ Username string // Username
+ Name string // Name
+ Surname string // Surname
+ Mail string // E-mail address
+ SSHPublicKey []string // SSH Public Key
+ IsAdmin bool // if user is administrator
+ IsRestricted bool // if user is restricted
+ LowerName string // LowerName
+ Avatar []byte
+ Groups container.Set[string]
}
func (source *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -196,9 +194,8 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
}
// List all group memberships of a user
-func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) []string {
- var ldapGroups []string
- var searchFilter string
+func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) container.Set[string] {
+ ldapGroups := make(container.Set[string])
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
if !ok {
@@ -210,12 +207,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
return ldapGroups
}
+ var searchFilter string
if applyGroupFilter {
searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid))
} else {
searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
}
-
result, err := l.Search(ldap.NewSearchRequest(
groupDN,
ldap.ScopeWholeSubtree,
@@ -237,44 +234,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
log.Error("LDAP search was successful, but found no DN!")
continue
}
- ldapGroups = append(ldapGroups, entry.DN)
+ ldapGroups.Add(entry.DN)
}
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 {
if strings.ToLower(source.UserUID) == "dn" {
return entry.DN
@@ -399,23 +364,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
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 {
sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
}
@@ -431,6 +379,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
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 {
// binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd)
@@ -440,17 +399,16 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
}
return &SearchResult{
- LowerName: strings.ToLower(username),
- Username: username,
- Name: firstname,
- Surname: surname,
- Mail: mail,
- SSHPublicKey: sshPublicKey,
- IsAdmin: isAdmin,
- IsRestricted: isRestricted,
- Avatar: Avatar,
- LdapTeamAdd: teamsToAdd,
- LdapTeamRemove: teamsToRemove,
+ LowerName: strings.ToLower(username),
+ Username: username,
+ Name: firstname,
+ Surname: surname,
+ Mail: mail,
+ SSHPublicKey: sshPublicKey,
+ IsAdmin: isAdmin,
+ IsRestricted: isRestricted,
+ Avatar: Avatar,
+ Groups: usersLdapGroups,
}
}
@@ -512,33 +470,29 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
result := make([]*SearchResult, 0, len(sr.Entries))
for _, v := range sr.Entries {
- teamsToAdd := make(map[string][]string)
- teamsToRemove := make(map[string][]string)
-
+ var usersLdapGroups container.Set[string]
if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(v)
if source.GroupFilter != "" {
- usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
+ usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if len(usersLdapGroups) == 0 {
continue
}
}
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
- usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
- teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
+ usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
}
}
user := &SearchResult{
- Username: v.GetAttributeValue(source.AttributeUsername),
- Name: v.GetAttributeValue(source.AttributeName),
- Surname: v.GetAttributeValue(source.AttributeSurname),
- Mail: v.GetAttributeValue(source.AttributeMail),
- IsAdmin: checkAdmin(l, source, v.DN),
- LdapTeamAdd: teamsToAdd,
- LdapTeamRemove: teamsToRemove,
+ Username: v.GetAttributeValue(source.AttributeUsername),
+ Name: v.GetAttributeValue(source.AttributeName),
+ Surname: v.GetAttributeValue(source.AttributeSurname),
+ Mail: v.GetAttributeValue(source.AttributeMail),
+ IsAdmin: checkAdmin(l, source, v.DN),
+ Groups: usersLdapGroups,
}
if !user.IsAdmin {
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 73e8309aca..4571ff6540 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -13,8 +13,10 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
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/util"
+ source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user"
)
@@ -65,6 +67,11 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team)
+ groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
+ if err != nil {
+ return err
+ }
+
for _, su := range sr {
select {
case <-ctx.Done():
@@ -173,7 +180,9 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
}
// Synchronize LDAP groups with organization and team memberships
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
- source.SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, orgCache, teamCache)
+ if err := source_service.SyncGroupsToTeamsCached(ctx, usr, su.Groups, groupTeamMapping, source.GroupTeamMapRemoval, orgCache, teamCache); err != nil {
+ log.Error("SyncGroupsToTeamsCached: %v", err)
+ }
}
}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 0abebc04ec..675005e55a 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -8,13 +8,6 @@ import (
"code.gitea.io/gitea/modules/json"
)
-// ________ _____ __ .__ ________
-// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
-// / | \ / /_\ \| | \ __\ | \ / ____/
-// / | \/ | \ | /| | | Y \/ \
-// \_______ /\____|__ /____/ |__| |___| /\_______ \
-// \/ \/ \/ \/
-
// Source holds configuration for the OAuth2 login source.
type Source struct {
Provider string
@@ -24,13 +17,15 @@ type Source struct {
CustomURLMapping *CustomURLMapping
IconURL string
- Scopes []string
- RequiredClaimName string
- RequiredClaimValue string
- GroupClaimName string
- AdminGroup string
- RestrictedGroup string
- SkipLocalTwoFA bool `json:",omitempty"`
+ Scopes []string
+ RequiredClaimName string
+ RequiredClaimValue string
+ GroupClaimName string
+ AdminGroup string
+ GroupTeamMap string
+ GroupTeamMapRemoval bool
+ RestrictedGroup string
+ SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
new file mode 100644
index 0000000000..20b6095345
--- /dev/null
+++ b/services/auth/source/source_group_sync.go
@@ -0,0 +1,116 @@
+// 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
+}
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 0cede07f95..5625aa1e2e 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -72,13 +72,15 @@ type AuthenticationForm struct {
Oauth2GroupClaimName string
Oauth2AdminGroup string
Oauth2RestrictedGroup string
+ Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
+ Oauth2GroupTeamMapRemoval bool
SkipLocalTwoFA bool
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string
- GroupTeamMap string
+ GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
}
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 91e4b1df52..a3c94a6cc2 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -361,6 +361,14 @@
<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}}">
</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}}
<!-- SSPI -->
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index b44eb799b9..8d199854ae 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -52,7 +52,7 @@
</div>
<div class="field">
<label for="restricted_filter">{{.locale.Tr "admin.auths.restricted_filter"}}</label>
- <input id="restricted_filter" name="admin_filter" value="{{.restricted_filter}}">
+ <input id="restricted_filter" name="restricted_filter" value="{{.restricted_filter}}">
<p class="help">{{.locale.Tr "admin.auths.restricted_filter_helper"}}</p>
</div>
<div class="field">
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl
index 166373a324..85c77343a5 100644
--- a/templates/admin/auth/source/oauth.tmpl
+++ b/templates/admin/auth/source/oauth.tmpl
@@ -98,4 +98,12 @@
<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}}">
</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>
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 9dfa4c73d8..883c9d80a3 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -112,23 +112,14 @@ func getLDAPServerPort() string {
return port
}
-func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) {
- groupTeamMapRemoval := "off"
- groupTeamMap := ""
- if len(groupMapParams) == 2 {
- groupTeamMapRemoval = groupMapParams[0]
- groupTeamMap = groupMapParams[1]
- }
-
+func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string {
// Modify user filter to test group filter explicitly
userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))"
if groupFilter != "" {
userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))"
}
- session := loginUser(t, "user1")
- csrf := GetCSRF(t, session, "/admin/auths/new")
- req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{
+ return map[string]string{
"_csrf": csrf,
"type": "2",
"name": "ldap",
@@ -154,7 +145,19 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupM
"group_team_map": groupTeamMap,
"group_team_map_removal": groupTeamMapRemoval,
"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)
}
@@ -202,26 +205,7 @@ func TestLDAPAuthChange(t *testing.T) {
binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com")
- 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",
- })
+ req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off"))
session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequest(t, "GET", href)
@@ -395,7 +379,7 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.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"]}}`)
- org, err := organization.GetOrgByName("org26")
+ org, err := organization.GetOrgByName(db.DefaultContext, "org26")
assert.NoError(t, err)
team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
assert.NoError(t, err)
@@ -440,7 +424,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
}
defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
- org, err := organization.GetOrgByName("org26")
+ org, err := organization.GetOrgByName(db.DefaultContext, "org26")
assert.NoError(t, err)
team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
assert.NoError(t, err)
@@ -468,24 +452,15 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
assert.False(t, isMember, "User membership should have been removed from team")
}
-// Login should work even if Team Group Map contains a broken JSON
-func TestBrokenLDAPMapUserSignin(t *testing.T) {
+func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) {
if skipLDAPTests() {
t.Skip()
return
}
defer tests.PrepareTestEnv(t)()
- addAuthSourceLDAP(t, "", "", "on", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`)
- u := gitLDAPUsers[0]
-
- session := loginUserWithPassword(t, u.UserName, u.Password)
- 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())
+ session := loginUser(t, "user1")
+ 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.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok
}