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.  --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		
							parent
							
								
									2c6cc0b8c9
								
							
						
					
					
						commit
						e8186f1c0f
					
				
					 34 changed files with 504 additions and 427 deletions
				
			
		
							
								
								
									
										17
									
								
								cmd/admin.go
									
										
									
									
									
								
							
							
						
						
									
										17
									
								
								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{}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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`:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
// GetTeam returns named team of organization.
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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/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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 != "" {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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"
 | 
			
		||||
	"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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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) {
 | 
			
		||||
		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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
	"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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 -->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue