fix: deactivate and ban revoke sessions and API tokens, and /refresh refuses deactivated, banned and cut-off subjects
classes.RevokeUserAccess stamps users.tokens_valid_after (whole seconds) and revokes the users' personal API tokens; DeactivateUsers and BanUsers call it for every selected user. Restore and unban revive nothing, and a deactivated user who signs in again gets a fresh working token. POST /refresh now loads the token's subject after bouncer.Refresh and refuses a missing, deactivated or banned user and a token issued before tokens_valid_after with the existing 401 body; a refused refresh stamps no last_seen. Suspension is deliberately not checked.
This commit is contained in:
@@ -90,8 +90,41 @@ func ActivateUsers(ctx context.Context, db *gorm.DB, users []*models.User) (int,
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// RevokeUserAccess ends every session JWT issued before now and every
|
||||
// personal API token of userIDs: it stamps users.tokens_valid_after with the
|
||||
// current time truncated to whole seconds (soft-deleted rows included) and
|
||||
// sets revoked_at on each of their user_api_tokens rows that is not revoked
|
||||
// yet (earlier revoked_at values stay). The jwt guard, RequestUser and
|
||||
// /refresh refuse a token whose iat precedes tokens_valid_after; the
|
||||
// user.api_token guard refuses a revoked row. JWT iat has whole-second
|
||||
// precision, so the cutoff is truncated: a token minted right after the
|
||||
// revocation (a reactivating login) is valid, and so is one minted earlier
|
||||
// within that same second. An empty list writes nothing. It takes the
|
||||
// handle of the caller's transaction. DeactivateUsers and BanUsers call it;
|
||||
// an application may call it for its own offboarding, and after writing a
|
||||
// ban directly to user_throttle.
|
||||
func RevokeUserAccess(ctx context.Context, db *gorm.DB, userIDs []uint) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("golem15.user: gorm handle is missing")
|
||||
}
|
||||
if len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
cutoff := time.Now().Truncate(time.Second)
|
||||
if err := fresh(ctx, db).Unscoped().Model(&models.User{}).Where("id IN ?", userIDs).UpdateColumn("tokens_valid_after", cutoff).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return fresh(ctx, db).Model(&models.APIToken{}).
|
||||
Where("user_id IN ? AND revoked_at IS NULL", userIDs).
|
||||
UpdateColumn("revoked_at", cutoff).Error
|
||||
}
|
||||
|
||||
// DeactivateUsers soft-deletes the users that are not deactivated yet, the
|
||||
// PHP $user->delete(), and returns how many it changed.
|
||||
// PHP $user->delete(), and returns how many it changed. It then ends the
|
||||
// sessions and personal API tokens of every selected user, already
|
||||
// deactivated ones included (RevokeUserAccess), so a deactivated account
|
||||
// keeps no working credential: its owner signs in again, which restores
|
||||
// it, and gets a fresh token. Restoring revives no revoked token.
|
||||
func DeactivateUsers(ctx context.Context, db *gorm.DB, users []*models.User) (int, error) {
|
||||
if db == nil {
|
||||
return 0, fmt.Errorf("golem15.user: gorm handle is missing")
|
||||
@@ -109,6 +142,13 @@ func DeactivateUsers(ctx context.Context, db *gorm.DB, users []*models.User) (in
|
||||
user.DeletedAt = gorm.DeletedAt{Time: now, Valid: true}
|
||||
changed++
|
||||
}
|
||||
ids, err := userIDs(db, users)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := RevokeUserAccess(ctx, db, ids); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
@@ -136,7 +176,9 @@ func RestoreUsers(ctx context.Context, db *gorm.DB, users []*models.User) (int,
|
||||
// BanUsers bans the users and returns how many were not banned before. A ban
|
||||
// is set on every user_throttle row of the user, and a row without an IP
|
||||
// address is created when the user has none, so a login from any address is
|
||||
// refused (RejectIfThrottled falls back to that row).
|
||||
// refused (RejectIfThrottled falls back to that row). It then ends the
|
||||
// sessions and personal API tokens of every selected user, already banned
|
||||
// ones included (RevokeUserAccess); unbanning revives none of them.
|
||||
func BanUsers(ctx context.Context, db *gorm.DB, users []*models.User) (int, error) {
|
||||
ids, err := userIDs(db, users)
|
||||
if err != nil {
|
||||
@@ -164,6 +206,9 @@ func BanUsers(ctx context.Context, db *gorm.DB, users []*models.User) (int, erro
|
||||
changed++
|
||||
}
|
||||
}
|
||||
if err := RevokeUserAccess(ctx, db, ids); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user