Fix last admin check when syncing users (#34649)

Fix #34358
pull/31899/head^2
wxiaoguang 2025-06-10 04:57:45 +08:00 committed by GitHub
parent 92e7e98c56
commit 0082cb51fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 49 additions and 20 deletions

View File

@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) {
Location: optional.FromPtr(form.Location),
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: optional.FromPtr(form.Admin),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),

View File

@ -432,7 +432,7 @@ func EditUserPost(ctx *context.Context) {
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
IsActive: optional.Some(form.Active),
IsAdmin: optional.Some(form.Admin),
IsAdmin: user_service.UpdateOptionFieldFromValue(form.Admin),
AllowGitHook: optional.Some(form.AllowGitHook),
AllowImportLocal: optional.Some(form.AllowImportLocal),
MaxRepoCreation: optional.Some(form.MaxRepoCreation),

View File

@ -613,7 +613,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
if user_model.CountUsers(ctx, nil) == 1 {
opts := &user_service.UpdateOptions{
IsActive: optional.Some(true),
IsAdmin: optional.Some(true),
IsAdmin: user_service.UpdateOptionFieldFromValue(true),
SetLastLogin: true,
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {

View File

@ -193,8 +193,8 @@ func SignInOAuthCallback(ctx *context.Context) {
source := authSource.Cfg.(*oauth2.Source)
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
u.IsAdmin = isAdmin.ValueOrDefault(false)
u.IsRestricted = isRestricted.ValueOrDefault(false)
u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
@ -258,11 +258,11 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims)
}
func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin optional.Option[user_service.UpdateOptionField[bool]], isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
if source.AdminGroup != "" {
isAdmin = optional.Some(groups.Contains(source.AdminGroup))
isAdmin = user_service.UpdateOptionFieldFromSync(groups.Contains(source.AdminGroup))
}
if source.RestrictedGroup != "" {
isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))

View File

@ -58,7 +58,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
opts := &user_service.UpdateOptions{}
if source.AdminFilter != "" && user.IsAdmin != sr.IsAdmin {
// Change existing admin flag only if AdminFilter option is set
opts.IsAdmin = optional.Some(sr.IsAdmin)
opts.IsAdmin = user_service.UpdateOptionFieldFromSync(sr.IsAdmin)
}
if !sr.IsAdmin && source.RestrictedFilter != "" && user.IsRestricted != sr.IsRestricted {
// Change existing restricted flag only if RestrictedFilter option is set

View File

@ -162,7 +162,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
IsActive: optional.Some(true),
}
if source.AdminFilter != "" {
opts.IsAdmin = optional.Some(su.IsAdmin)
opts.IsAdmin = user_service.UpdateOptionFieldFromSync(su.IsAdmin)
}
// Change existing restricted flag only if RestrictedFilter option is set
if !su.IsAdmin && source.RestrictedFilter != "" {

View File

@ -15,6 +15,26 @@ import (
"code.gitea.io/gitea/modules/structs"
)
type UpdateOptionField[T any] struct {
FieldValue T
FromSync bool
}
func UpdateOptionFieldFromValue[T any](value T) optional.Option[UpdateOptionField[T]] {
return optional.Some(UpdateOptionField[T]{FieldValue: value})
}
func UpdateOptionFieldFromSync[T any](value T) optional.Option[UpdateOptionField[T]] {
return optional.Some(UpdateOptionField[T]{FieldValue: value, FromSync: true})
}
func UpdateOptionFieldFromPtr[T any](value *T) optional.Option[UpdateOptionField[T]] {
if value == nil {
return optional.None[UpdateOptionField[T]]()
}
return UpdateOptionFieldFromValue(*value)
}
type UpdateOptions struct {
KeepEmailPrivate optional.Option[bool]
FullName optional.Option[string]
@ -32,7 +52,7 @@ type UpdateOptions struct {
DiffViewStyle optional.Option[string]
AllowCreateOrganization optional.Option[bool]
IsActive optional.Option[bool]
IsAdmin optional.Option[bool]
IsAdmin optional.Option[UpdateOptionField[bool]]
EmailNotificationsPreference optional.Option[string]
SetLastLogin bool
RepoAdminChangeTeamAccess optional.Option[bool]
@ -111,13 +131,18 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
cols = append(cols, "is_restricted")
}
if opts.IsAdmin.Has() {
if !opts.IsAdmin.Value() && user_model.IsLastAdminUser(ctx, u) {
if opts.IsAdmin.Value().FieldValue /* true */ {
u.IsAdmin = opts.IsAdmin.Value().FieldValue // set IsAdmin=true
cols = append(cols, "is_admin")
} else if !user_model.IsLastAdminUser(ctx, u) /* not the last admin */ {
u.IsAdmin = opts.IsAdmin.Value().FieldValue // it's safe to change it from false to true (not the last admin)
cols = append(cols, "is_admin")
} else /* IsAdmin=false but this is the last admin user */ { //nolint
if !opts.IsAdmin.Value().FromSync {
return user_model.ErrDeleteLastAdminUser{UID: u.ID}
}
u.IsAdmin = opts.IsAdmin.Value()
cols = append(cols, "is_admin")
// else: syncing from external-source, this user is the last admin, so skip the "IsAdmin=false" change
}
}
if opts.Visibility.Has() {

View File

@ -22,7 +22,11 @@ func TestUpdateUser(t *testing.T) {
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.Error(t, UpdateUser(db.DefaultContext, admin, &UpdateOptions{
IsAdmin: optional.Some(false),
IsAdmin: UpdateOptionFieldFromValue(false),
}))
assert.NoError(t, UpdateUser(db.DefaultContext, admin, &UpdateOptions{
IsAdmin: UpdateOptionFieldFromSync(false),
}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
@ -38,7 +42,7 @@ func TestUpdateUser(t *testing.T) {
MaxRepoCreation: optional.Some(10),
IsRestricted: optional.Some(true),
IsActive: optional.Some(false),
IsAdmin: optional.Some(true),
IsAdmin: UpdateOptionFieldFromValue(true),
Visibility: optional.Some(structs.VisibleTypePrivate),
KeepActivityPrivate: optional.Some(true),
Language: optional.Some("lang"),
@ -60,7 +64,7 @@ func TestUpdateUser(t *testing.T) {
assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation)
assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted)
assert.Equal(t, opts.IsActive.Value(), user.IsActive)
assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin)
assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin)
assert.Equal(t, opts.Visibility.Value(), user.Visibility)
assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
assert.Equal(t, opts.Language.Value(), user.Language)
@ -80,7 +84,7 @@ func TestUpdateUser(t *testing.T) {
assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation)
assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted)
assert.Equal(t, opts.IsActive.Value(), user.IsActive)
assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin)
assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin)
assert.Equal(t, opts.Visibility.Value(), user.Visibility)
assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
assert.Equal(t, opts.Language.Value(), user.Language)