fix: access-revoked event on ban and password changes, banned users lose their link, token-specific MCP revoke

- AccessRevokedEvent and FireAccessRevoked: fired in the write's transaction
  by the ban action (newly banned users only), the password reset, an
  administrator setting a password and the self-service password change;
  a revoked subject event per user after commit
- LinkedBackendUser answers no administrator for a banned user
- FireSubjectEvent / FireSubjectEventAfterCommit carry TokenID or ClientID;
  DELETE /tokens/{id} ends only that token's MCP sessions
- the MCP personal-token path stamps last_used_ip from towel.ClientIP
- BackendLinksFor replaces the duplicate lookups; requestUsable uses
  IsUsableAt
This commit is contained in:
Jakub Zych
2026-10-23 16:47:56 +02:00
parent 32f92591d6
commit 970719ce75
13 changed files with 242 additions and 63 deletions

View File

@@ -244,8 +244,15 @@ func (c usersAdminController) FormBeforeUpdate(ctx context.Context, model any) e
// user's own change-password does. The cutoff is truncated to whole
// seconds, as in RevokeUserAccess, because JWT iat has whole-second
// precision, so the user's next login in that second is not refused.
now := time.Now().Truncate(time.Second)
// OAuth grants created before the untruncated instant are revoked in
// the save's transaction (the access-revoked event).
at := time.Now()
now := at.Truncate(time.Second)
user.TokensValidAfter = &now
tx, _ := cabana.TxFromContext(ctx)
if err := classes.FireAccessRevoked(ctx, c.application(), tx, []uint{user.ID}, at, classes.AccessRevokedPasswordSetByAdmin); err != nil {
return err
}
}
return nil
}
@@ -631,11 +638,50 @@ func (c usersAdminController) AdminBulkActions() []pact.AdminBulkAction {
bulk("activate", classes.ActivateUsers),
bulk("deactivate", c.deactivateUsers),
bulk("restore", classes.RestoreUsers),
bulk("ban", classes.BanUsers),
bulk("ban", c.banUsers),
bulk("unban", classes.UnbanUsers),
}
}
// banUsers runs classes.BanUsers unchanged in the action's transaction and
// fires the access-revoked event there for the users it newly banned (banned
// after, not before), so their OAuth grants are revoked with the ban and
// their MCP sessions end once it commits. A listener error rolls the action
// back.
func (c usersAdminController) banUsers(ctx context.Context, tx *gorm.DB, users []*models.User) (int, error) {
ids := make([]uint, 0, len(users))
for _, user := range users {
if user != nil && user.ID != 0 {
ids = append(ids, user.ID)
}
}
before, _, err := classes.ThrottleStates(ctx, tx, ids)
if err != nil {
return 0, err
}
at := time.Now()
changed, err := classes.BanUsers(ctx, tx, users)
if err != nil {
return 0, err
}
after, _, err := classes.ThrottleStates(ctx, tx, ids)
if err != nil {
return 0, err
}
newly := make([]uint, 0, len(ids))
for _, id := range ids {
if after[id] && !before[id] {
newly = append(newly, id)
}
}
if len(newly) > 0 {
if err := classes.FireAccessRevoked(ctx, c.application(), tx, newly, at, classes.AccessRevokedBan); err != nil {
return 0, err
}
}
return changed, nil
}
// deactivateUsers runs classes.DeactivateUsers unchanged and then fires
// user.Deactivating, in the action's transaction, for every selected user
// that was not deactivated before. A listener error rolls the action back.