feat: password-change hook and the reset-2FA admin slot
The D-07 password-change hook is the existing AccessRevokedEvent, fired inside the write's transaction by a reset, the user's own change and an administrator's password; (*AccessRevokedEvent).PasswordChanged reports those three reasons, and the login rehash fires nothing. The Users screen declares the record action reset_two_factor (danger) behind the new permission golem15.users.reset_two_factor, which no role has by default. It is offered only when a two-factor plugin publishes a classes.TwoFactorAdmin and the user has a second factor; Run calls the service's Reset in the action's transaction.
This commit is contained in:
@@ -119,7 +119,7 @@ func Login(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
// The rehash runs before the login gates, so a user who is always
|
||||
// challenged still gets the stronger hash. It is not a password
|
||||
// change: it fires no PasswordChangedEvent. Unscoped, because a
|
||||
// change: it fires no AccessRevokedEvent (D-07). Unscoped, because a
|
||||
// deactivated account is restored only after the gates.
|
||||
cost := 10
|
||||
if app.Config != nil && app.Config.Int("golem15.user.password.bcrypt_cost") > 0 {
|
||||
|
||||
@@ -14,7 +14,7 @@ update:
|
||||
preview:
|
||||
headerPartial: hint
|
||||
|
||||
recordActions: [activate, unban, unsuspend]
|
||||
recordActions: [activate, unban, unsuspend, reset_two_factor]
|
||||
|
||||
messages:
|
||||
create: golem15.user::lang.users.new_user
|
||||
|
||||
@@ -719,8 +719,15 @@ func (c usersAdminController) throttleState(ctx context.Context, userID uint) (b
|
||||
}
|
||||
|
||||
// AdminRecordActions are the PHP preview handlers onActivate, onUnban and
|
||||
// onUnsuspendUser. Each is offered only while it applies; the checks only
|
||||
// read. There is no impersonate action and no guest action.
|
||||
// onUnsuspendUser, plus reset_two_factor. Each is offered only while it
|
||||
// applies; the checks only read. There is no impersonate action and no
|
||||
// guest action.
|
||||
//
|
||||
// reset_two_factor ("Reset 2FA") needs classes.PermissionResetTwoFactor and
|
||||
// asks for a danger confirmation. It is offered only when a two-factor
|
||||
// plugin published a classes.TwoFactorAdmin (looked up per request) and the
|
||||
// user has a second factor; Run hands the record to that service's Reset in
|
||||
// the action's transaction.
|
||||
func (c usersAdminController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
asUser := func(record any) (*models.User, error) {
|
||||
user, ok := record.(*models.User)
|
||||
@@ -785,9 +792,55 @@ func (c usersAdminController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
func(ctx context.Context, tx *gorm.DB, user *models.User) error {
|
||||
return classes.UnsuspendUser(ctx, tx, user.ID)
|
||||
}),
|
||||
c.resetTwoFactorAction(action),
|
||||
}
|
||||
}
|
||||
|
||||
// resetTwoFactorAction builds the reset_two_factor record action with the
|
||||
// shared action builder, behind classes.PermissionResetTwoFactor and marked
|
||||
// as dangerous.
|
||||
func (c usersAdminController) resetTwoFactorAction(action func(name, label, confirm, success string,
|
||||
applies func(ctx context.Context, user *models.User) (bool, error),
|
||||
run func(ctx context.Context, tx *gorm.DB, user *models.User) error,
|
||||
) pact.AdminRecordAction) pact.AdminRecordAction {
|
||||
reset := action("reset_two_factor", "reset_two_factor", "reset_two_factor_confirm", "reset_two_factor_success",
|
||||
func(ctx context.Context, user *models.User) (bool, error) {
|
||||
svc, ok := c.twoFactorAdmin()
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
db := requestDB(ctx, c.db)
|
||||
if db == nil {
|
||||
return false, errors.New("users admin: gorm handle is missing")
|
||||
}
|
||||
return svc.HasTwoFactor(ctx, db, user.ID)
|
||||
},
|
||||
func(ctx context.Context, tx *gorm.DB, user *models.User) error {
|
||||
svc, ok := c.twoFactorAdmin()
|
||||
if !ok {
|
||||
return errors.New("users admin: no two-factor plugin publishes classes.TwoFactorAdmin")
|
||||
}
|
||||
return svc.Reset(ctx, tx, user)
|
||||
})
|
||||
reset.Permissions = []string{classes.PermissionResetTwoFactor}
|
||||
reset.Danger = true
|
||||
return reset
|
||||
}
|
||||
|
||||
// twoFactorAdmin looks up the classes.TwoFactorAdmin a two-factor plugin
|
||||
// published, on every call, so a plugin that boots later is still found.
|
||||
func (c usersAdminController) twoFactorAdmin() (classes.TwoFactorAdmin, bool) {
|
||||
app := c.application()
|
||||
if app == nil {
|
||||
return nil, false
|
||||
}
|
||||
svc, ok := app.Lookup[classes.TwoFactorAdmin]()
|
||||
if !ok || svc == nil {
|
||||
return nil, false
|
||||
}
|
||||
return svc, true
|
||||
}
|
||||
|
||||
// usersHintView is the view model of controllers/users/_hint.htm, the status
|
||||
// callout above the preview: the callout kind (danger or warning) and the
|
||||
// phrase keys of its title and text. It is empty when no state applies, and
|
||||
|
||||
Reference in New Issue
Block a user