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
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 != "" {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue