feat: allow sync quota groups with oauth2 auth source (#8554)

Implements synchronizing an external user's quota group with provided OAuth2 claim.

This functionality will allow system administrators to manage user's quota groups automatically.

Documentation is at forgejo/docs#1337

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8554
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: thezzisu <thezzisu@gmail.com>
Co-committed-by: thezzisu <thezzisu@gmail.com>
This commit is contained in:
thezzisu 2025-12-01 14:12:00 +01:00 committed by Gusted
parent 1000a0da3a
commit e31d67e0aa
15 changed files with 585 additions and 3 deletions

View file

@ -191,6 +191,9 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
GroupTeamMap: form.Oauth2GroupTeamMap,
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
AllowUsernameChange: form.AllowUsernameChange,
QuotaGroupClaimName: form.Oauth2QuotaGroupClaimName,
QuotaGroupMap: form.Oauth2QuotaGroupMap,
QuotaGroupMapRemoval: form.Oauth2QuotaGroupMapRemoval,
}
}

View file

@ -1096,6 +1096,11 @@ func SignInOAuthCallback(ctx *context.Context) {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
if err := syncGroupsToQuotaGroups(ctx, source, &gothUser, u); err != nil {
ctx.ServerError("SyncGroupsToQuotaGroups", err)
return
}
} else {
// no existing user is found, request attach or new account
showLinkingLogin(ctx, gothUser)
@ -1140,6 +1145,23 @@ func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *go
return nil
}
func syncGroupsToQuotaGroups(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error {
if source.QuotaGroupMap != "" || source.QuotaGroupMapRemoval {
quotaGroupMapping, err := auth_module.UnmarshalQuotaGroupMapping(source.QuotaGroupMap)
if err != nil {
return err
}
groups := getClaimedQuotaGroups(source, gothUser)
if err := source_service.SyncGroupsToQuotaGroups(ctx, u, groups, quotaGroupMapping, source.QuotaGroupMapRemoval); 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 {
@ -1149,6 +1171,15 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims)
}
func getClaimedQuotaGroups(source *oauth2.Source, gothUser *goth.User) container.Set[string] {
groupClaims, has := gothUser.RawData[source.QuotaGroupClaimName]
if !has {
return nil
}
return claimValueToStringSet(groupClaims)
}
func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
@ -1262,8 +1293,14 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
ctx.ServerError("UnmarshalGroupTeamMapping", err)
return
}
quotaGroupMapping, err := auth_module.UnmarshalQuotaGroupMapping(oauth2Source.QuotaGroupMap)
if err != nil {
ctx.ServerError("UnmarshalQuotaGroupMapping", err)
return
}
groups := getClaimedGroups(oauth2Source, &gothUser)
quotaGroups := getClaimedQuotaGroups(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.
@ -1291,6 +1328,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
}
}
if oauth2Source.QuotaGroupMap != "" || oauth2Source.QuotaGroupMapRemoval {
if err := source_service.SyncGroupsToQuotaGroups(ctx, u, quotaGroups, quotaGroupMapping, oauth2Source.QuotaGroupMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToQuotaGroups", err)
return
}
}
// update external user information
if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) {
@ -1329,6 +1373,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
}
}
if oauth2Source.QuotaGroupMap != "" || oauth2Source.QuotaGroupMapRemoval {
if err := source_service.SyncGroupsToQuotaGroups(ctx, u, quotaGroups, quotaGroupMapping, oauth2Source.QuotaGroupMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToQuotaGroups", err)
return
}
}
if err := updateSession(ctx, nil, map[string]any{
// User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID,