Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map OIDC groups to Orgs/Teams #21441

Merged
merged 21 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7678456
Move code from module to service.
KN4CK3R Oct 12, 2022
df9e21c
Pass context as parameter.
KN4CK3R Oct 12, 2022
81f8c4c
Refactor group sync.
KN4CK3R Oct 12, 2022
c41d51d
Add OAuth mapping.
KN4CK3R Oct 13, 2022
146fd71
Fix tests.
KN4CK3R Oct 13, 2022
80dfd9f
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Oct 13, 2022
ab565b2
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Oct 18, 2022
f8f1155
Merge branch 'main' into feature-oidc-group-mapping
KN4CK3R Oct 31, 2022
7e4b986
Merge branch 'main' into feature-oidc-group-mapping
KN4CK3R Nov 2, 2022
09d023a
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Nov 10, 2022
3267642
Merge branch 'feature-oidc-group-mapping' of https://github.com/KN4CK…
KN4CK3R Nov 10, 2022
518c004
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Nov 17, 2022
7184222
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Nov 23, 2022
830996f
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 8, 2022
0584661
Use new file header.
KN4CK3R Dec 8, 2022
f4c789f
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 19, 2022
8bd182c
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jan 13, 2023
636881b
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Feb 6, 2023
e14e099
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Feb 6, 2023
b7d7efc
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Feb 6, 2023
9f54810
Merge branch 'main' into feature-oidc-group-mapping
lunny Feb 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cmd/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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"),
}
}

Expand Down Expand Up @@ -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{}
Expand Down
2 changes: 2 additions & 0 deletions docs/content/doc/usage/command-line.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
20 changes: 6 additions & 14 deletions models/organization/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,14 @@ func (org *Organization) CanCreateOrgRepo(uid int64) (bool, error) {
return CanCreateOrgRepo(db.DefaultContext, org.ID, uid)
}

func (org *Organization) getTeam(ctx context.Context, name string) (*Team, error) {
return GetTeam(ctx, org.ID, name)
}

// GetTeam returns named team of organization.
func (org *Organization) GetTeam(name string) (*Team, error) {
return org.getTeam(db.DefaultContext, name)
}

func (org *Organization) getOwnerTeam(ctx context.Context) (*Team, error) {
return org.getTeam(ctx, OwnerTeamName)
func (org *Organization) GetTeam(ctx context.Context, name string) (*Team, error) {
return GetTeam(ctx, org.ID, name)
}

// GetOwnerTeam returns owner team of organization.
func (org *Organization) GetOwnerTeam() (*Team, error) {
return org.getOwnerTeam(db.DefaultContext)
func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) {
return org.GetTeam(ctx, OwnerTeamName)
}

// FindOrgTeams returns all teams of a given organization
Expand Down Expand Up @@ -342,15 +334,15 @@ 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}
}
u := &Organization{
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 {
Expand Down
16 changes: 8 additions & 8 deletions models/organization/org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -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))
}

Expand Down
22 changes: 22 additions & 0 deletions modules/auth/common.go
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 0 additions & 30 deletions modules/context/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
32 changes: 0 additions & 32 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion modules/context/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions modules/repository/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion modules/repository/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
24 changes: 21 additions & 3 deletions modules/validation/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"regexp"
"strings"

"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/git"

"gitea.com/go-chi/binding"
Expand All @@ -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
Expand All @@ -37,6 +37,7 @@ func AddBindingRules() {
addRegexPatternRule()
addGlobOrRegexPatternRule()
addUsernamePatternRule()
addValidGroupTeamMapRule()
}

func addGitRefNameBindingRule() {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions modules/web/middleware/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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,
Expand Down
Loading