mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-25 11:16:11 +01:00
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>
This commit is contained in:
parent
2c6cc0b8c9
commit
e8186f1c0f
17
cmd/admin.go
17
cmd/admin.go
|
@ -372,6 +372,15 @@ 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{
|
||||||
|
@ -853,6 +862,8 @@ 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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -935,6 +946,12 @@ 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{}
|
||||||
|
|
|
@ -137,6 +137,8 @@ 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`:
|
||||||
|
|
|
@ -110,22 +110,14 @@ func (org *Organization) CanCreateOrgRepo(uid int64) (bool, error) {
|
||||||
return CanCreateOrgRepo(db.DefaultContext, org.ID, uid)
|
return CanCreateOrgRepo(db.DefaultContext, org.ID, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (org *Organization) getTeam(ctx context.Context, name string) (*Team, error) {
|
// GetTeam returns named team of organization.
|
||||||
|
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() (*Team, error) {
|
func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) {
|
||||||
return org.getOwnerTeam(db.DefaultContext)
|
return org.GetTeam(ctx, OwnerTeamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindOrgTeams returns all teams of a given organization
|
// 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.
|
// 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 {
|
if len(name) == 0 {
|
||||||
return nil, ErrOrgNotExist{0, name}
|
return nil, ErrOrgNotExist{0, name}
|
||||||
}
|
}
|
||||||
|
@ -350,7 +342,7 @@ func GetOrgByName(name string) (*Organization, error) {
|
||||||
LowerName: strings.ToLower(name),
|
LowerName: strings.ToLower(name),
|
||||||
Type: user_model.UserTypeOrganization,
|
Type: user_model.UserTypeOrganization,
|
||||||
}
|
}
|
||||||
has, err := db.GetEngine(db.DefaultContext).Get(u)
|
has, err := db.GetEngine(ctx).Get(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
|
|
|
@ -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("team1")
|
team, err := org.GetTeam(db.DefaultContext, "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("does not exist")
|
_, err = org.GetTeam(db.DefaultContext, "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("team")
|
_, err = nonOrg.GetTeam(db.DefaultContext, "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()
|
team, err := org.GetOwnerTeam(db.DefaultContext)
|
||||||
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()
|
_, err = nonOrg.GetOwnerTeam(db.DefaultContext)
|
||||||
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("user3")
|
org, err := organization.GetOrgByName(db.DefaultContext, "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("user2") // user2 is an individual
|
_, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual
|
||||||
assert.True(t, organization.IsErrOrgNotExist(err))
|
assert.True(t, organization.IsErrOrgNotExist(err))
|
||||||
|
|
||||||
_, err = organization.GetOrgByName("") // corner case
|
_, err = organization.GetOrgByName(db.DefaultContext, "") // corner case
|
||||||
assert.True(t, organization.IsErrOrgNotExist(err))
|
assert.True(t, organization.IsErrOrgNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
modules/auth/common.go
Normal file
22
modules/auth/common.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ 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
|
||||||
|
@ -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
|
// 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 {
|
||||||
|
|
|
@ -36,7 +36,6 @@ 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"
|
||||||
|
@ -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.
|
// 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)
|
||||||
|
|
|
@ -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(orgName)
|
ctx.Org.Organization, err = organization.GetOrgByName(ctx, 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)
|
||||||
|
|
|
@ -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()
|
ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
|
||||||
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()
|
ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
|
||||||
assert.NoError(t, err, "GetOwnerTeam")
|
assert.NoError(t, err, "GetOwnerTeam")
|
||||||
|
|
||||||
// Create teams and check repositories.
|
// Create teams and check repositories.
|
||||||
|
|
|
@ -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()
|
t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ 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"
|
||||||
|
@ -17,15 +18,14 @@ 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,6 +37,7 @@ func AddBindingRules() {
|
||||||
addRegexPatternRule()
|
addRegexPatternRule()
|
||||||
addGlobOrRegexPatternRule()
|
addGlobOrRegexPatternRule()
|
||||||
addUsernamePatternRule()
|
addUsernamePatternRule()
|
||||||
|
addValidGroupTeamMapRule()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addGitRefNameBindingRule() {
|
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 {
|
func portOnly(hostport string) string {
|
||||||
colon := strings.IndexByte(hostport, ':')
|
colon := strings.IndexByte(hostport, ':')
|
||||||
if colon == -1 {
|
if colon == -1 {
|
||||||
|
|
|
@ -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)
|
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 != "" {
|
||||||
|
|
|
@ -477,6 +477,7 @@ 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.
|
||||||
|
@ -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_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
|
||||||
|
|
|
@ -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.Params(":org"))
|
ctx.Org.Organization, err = organization.GetOrgByName(ctx, 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(context.APIAuth(group))
|
m.Use(auth.APIAuth(group))
|
||||||
|
|
||||||
m.Use(context.ToggleAPI(&context.ToggleOptions{
|
m.Use(context.ToggleAPI(&context.ToggleOptions{
|
||||||
SignInRequired: setting.Service.RequireSignInView,
|
SignInRequired: setting.Service.RequireSignInView,
|
||||||
|
|
|
@ -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(*form.Organization)
|
org, err := organization.GetOrgByName(ctx, *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)
|
||||||
|
|
|
@ -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.Params(":org"))
|
org, err := organization.GetOrgByName(ctx, 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)
|
||||||
|
|
|
@ -204,6 +204,8 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ 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"
|
||||||
|
|
||||||
|
@ -267,5 +268,11 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,9 @@ 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"
|
||||||
|
@ -27,6 +29,7 @@ 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"
|
||||||
|
@ -963,12 +966,19 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||||
IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm),
|
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) {
|
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)
|
||||||
|
@ -979,7 +989,7 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||||
handleOAuth2SignIn(ctx, authSource, u, gothUser)
|
handleOAuth2SignIn(ctx, authSource, u, gothUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func claimValueToStringSlice(claimValue interface{}) []string {
|
func claimValueToStringSet(claimValue interface{}) container.Set[string] {
|
||||||
var groups []string
|
var groups []string
|
||||||
|
|
||||||
switch rawGroup := claimValue.(type) {
|
switch rawGroup := claimValue.(type) {
|
||||||
|
@ -993,37 +1003,45 @@ func claimValueToStringSlice(claimValue interface{}) []string {
|
||||||
str := fmt.Sprintf("%s", rawGroup)
|
str := fmt.Sprintf("%s", rawGroup)
|
||||||
groups = strings.Split(str, ",")
|
groups = strings.Split(str, ",")
|
||||||
}
|
}
|
||||||
return groups
|
return container.SetOf(groups...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUserGroupClaims(loginSource *auth.Source, u *user_model.User, gothUser *goth.User) bool {
|
func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error {
|
||||||
source := loginSource.Cfg.(*oauth2.Source)
|
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
|
||||||
if source.GroupClaimName == "" || (source.AdminGroup == "" && source.RestrictedGroup == "") {
|
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
|
||||||
return false
|
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]
|
groupClaims, has := gothUser.RawData[source.GroupClaimName]
|
||||||
if !has {
|
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
|
wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
|
||||||
|
|
||||||
if source.AdminGroup != "" {
|
if source.AdminGroup != "" {
|
||||||
u.IsAdmin = false
|
u.IsAdmin = groups.Contains(source.AdminGroup)
|
||||||
}
|
}
|
||||||
if source.RestrictedGroup != "" {
|
if source.RestrictedGroup != "" {
|
||||||
u.IsRestricted = false
|
u.IsRestricted = groups.Contains(source.RestrictedGroup)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -1070,6 +1088,15 @@ 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 {
|
||||||
|
@ -1088,7 +1115,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||||
u.SetLastLogin()
|
u.SetLastLogin()
|
||||||
|
|
||||||
// Update GroupClaims
|
// Update GroupClaims
|
||||||
changed := setUserGroupClaims(source, u, &gothUser)
|
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, 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")
|
||||||
|
@ -1099,6 +1126,13 @@ 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) {
|
||||||
|
@ -1121,7 +1155,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
changed := setUserGroupClaims(source, u, &gothUser)
|
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, 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)
|
||||||
|
@ -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{}{
|
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,
|
||||||
|
@ -1188,15 +1229,9 @@ func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, res
|
||||||
}
|
}
|
||||||
|
|
||||||
if oauth2Source.RequiredClaimValue != "" {
|
if oauth2Source.RequiredClaimValue != "" {
|
||||||
groups := claimValueToStringSlice(claimInterface)
|
groups := claimValueToStringSet(claimInterface)
|
||||||
found := false
|
|
||||||
for _, group := range groups {
|
if !groups.Contains(oauth2Source.RequiredClaimValue) {
|
||||||
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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ func RetrieveLabels(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
ctx.Data["OrgLabels"] = orgLabels
|
||||||
|
|
||||||
org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName)
|
org, err := organization.GetOrgByName(ctx, ctx.Repo.Owner.LowerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetOrgByName", err)
|
ctx.ServerError("GetOrgByName", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1006,7 +1006,7 @@ func AddTeamPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(name)
|
team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, 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"))
|
||||||
|
|
|
@ -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, 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
|
// 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)
|
||||||
|
|
60
services/auth/middleware.go
Normal file
60
services/auth/middleware.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -10,9 +10,10 @@ 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"
|
||||||
)
|
)
|
||||||
|
@ -64,61 +65,66 @@ 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) {
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
orgCache := make(map[string]*organization.Organization)
|
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
|
||||||
teamCache := make(map[string]*organization.Team)
|
if err != nil {
|
||||||
source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
|
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
|
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,26 +11,24 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"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
|
||||||
LdapTeamAdd map[string][]string // organizations teams to add
|
Groups container.Set[string]
|
||||||
LdapTeamRemove map[string][]string // organizations teams to remove
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (source *Source) sanitizedUserQuery(username string) (string, bool) {
|
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
|
// List all group memberships of a user
|
||||||
func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) []string {
|
func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) container.Set[string] {
|
||||||
var ldapGroups []string
|
ldapGroups := make(container.Set[string])
|
||||||
var searchFilter string
|
|
||||||
|
|
||||||
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
|
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -210,12 +207,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,
|
||||||
|
@ -237,44 +234,12 @@ 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 = append(ldapGroups, entry.DN)
|
ldapGroups.Add(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
|
||||||
|
@ -399,23 +364,6 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -431,6 +379,17 @@ 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)
|
||||||
|
@ -440,17 +399,16 @@ 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,
|
||||||
LdapTeamAdd: teamsToAdd,
|
Groups: usersLdapGroups,
|
||||||
LdapTeamRemove: teamsToRemove,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,33 +470,29 @@ 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 {
|
||||||
teamsToAdd := make(map[string][]string)
|
var usersLdapGroups container.Set[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),
|
||||||
LdapTeamAdd: teamsToAdd,
|
Groups: usersLdapGroups,
|
||||||
LdapTeamRemove: teamsToRemove,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.IsAdmin {
|
if !user.IsAdmin {
|
||||||
|
|
|
@ -13,8 +13,10 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,6 +67,11 @@ 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():
|
||||||
|
@ -173,7 +180,9 @@ 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) {
|
||||||
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,6 @@ 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
|
||||||
|
@ -24,13 +17,15 @@ 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
|
||||||
RestrictedGroup string
|
GroupTeamMap string
|
||||||
SkipLocalTwoFA bool `json:",omitempty"`
|
GroupTeamMapRemoval bool
|
||||||
|
RestrictedGroup string
|
||||||
|
SkipLocalTwoFA bool `json:",omitempty"`
|
||||||
|
|
||||||
// reference to the authSource
|
// reference to the authSource
|
||||||
authSource *auth.Source
|
authSource *auth.Source
|
||||||
|
|
116
services/auth/source/source_group_sync.go
Normal file
116
services/auth/source/source_group_sync.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -72,13 +72,15 @@ 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
|
GroupTeamMap string `binding:"ValidGroupTeamMap"`
|
||||||
GroupTeamMapRemoval bool
|
GroupTeamMapRemoval bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -361,6 +361,14 @@
|
||||||
<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 -->
|
||||||
|
|
|
@ -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="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>
|
<p class="help">{{.locale.Tr "admin.auths.restricted_filter_helper"}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
@ -98,4 +98,12 @@
|
||||||
<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>
|
||||||
|
|
|
@ -112,23 +112,14 @@ func getLDAPServerPort() string {
|
||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) {
|
func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]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))"
|
||||||
}
|
}
|
||||||
|
|
||||||
session := loginUser(t, "user1")
|
return map[string]string{
|
||||||
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",
|
||||||
|
@ -154,7 +145,19 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupM
|
||||||
"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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,26 +205,7 @@ 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, map[string]string{
|
req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off"))
|
||||||
"_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)
|
||||||
|
@ -395,7 +379,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("org26")
|
org, err := organization.GetOrgByName(db.DefaultContext, "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)
|
||||||
|
@ -440,7 +424,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("org26")
|
org, err := organization.GetOrgByName(db.DefaultContext, "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)
|
||||||
|
@ -468,24 +452,15 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login should work even if Team Group Map contains a broken JSON
|
func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) {
|
||||||
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"]}`)
|
|
||||||
|
|
||||||
u := gitLDAPUsers[0]
|
session := loginUser(t, "user1")
|
||||||
|
csrf := GetCSRF(t, session, "/admin/auths/new")
|
||||||
session := loginUserWithPassword(t, u.UserName, u.Password)
|
req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off"))
|
||||||
req := NewRequest(t, "GET", "/user/settings")
|
session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue