Files
sm-user-plugin/classes/subject_events.go
Jakub Zych 970719ce75 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
2026-10-23 16:47:56 +02:00

70 lines
2.9 KiB
Go

package classes
import (
"context"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/pact"
"gorm.io/gorm"
)
// Guard names the plugin's pact.MCPSubjectChanged events carry: the frontend
// user guard this plugin provides and the admin's guard (for the backend
// side of an account link).
const (
SubjectGuardUser = "user"
SubjectGuardBackend = "backend"
)
// FireSubjectEvent fires ev through the application's event bus at once, so
// the MCP endpoint re-lists the subject's tools or ends its sessions. A
// revoked event may carry TokenID (only the sessions one personal token
// opened end) or ClientID with UserID (only that OAuth client's sessions
// end). A missing application or bus, a zero UserID or an empty Kind is a
// no-op; a listener's error is logged and never returned, so the write that
// caused the event never fails because of it (every MCP request re-checks
// anyway).
func FireSubjectEvent(ctx context.Context, app *backpack.App, ev pact.MCPSubjectChanged) {
if app == nil || app.Events == nil || ev.UserID == 0 || ev.Kind == "" {
return
}
if ctx == nil {
ctx = context.Background()
}
event := ev
if err := app.Events.Fire(ctx, &event); err != nil {
pluginLog.WarnContext(ctx, "golem15.user: subject changed listener failed", "guard", ev.Guard, "user_id", ev.UserID, "kind", ev.Kind, "token_id", ev.TokenID, "client_id", ev.ClientID, "err", err)
}
}
// FireSubjectEventAfterCommit is FireSubjectEvent once the write
// transaction tx commits (lagoon.AfterCommit), so a listener that reads the
// database sees the change; nothing is fired when the transaction rolls
// back. A nil tx fires at once. ctx must be the context the transaction
// runs under (the one cabana hands a hook or an action).
func FireSubjectEventAfterCommit(ctx context.Context, app *backpack.App, tx *gorm.DB, ev pact.MCPSubjectChanged) {
if app == nil || app.Events == nil || ev.UserID == 0 || ev.Kind == "" {
return
}
if tx == nil {
FireSubjectEvent(ctx, app, ev)
return
}
lagoon.AfterCommit(ctx, tx, func(ctx context.Context, _ *gorm.DB) {
FireSubjectEvent(ctx, app, ev)
})
}
// FireSubjectChanged fires one *pact.MCPSubjectChanged (guard, user id,
// kind) at once: FireSubjectEvent for the whole subject.
func FireSubjectChanged(ctx context.Context, app *backpack.App, guard string, userID uint, kind string) {
FireSubjectEvent(ctx, app, pact.MCPSubjectChanged{Guard: guard, UserID: userID, Kind: kind})
}
// FireSubjectChangedAfterCommit is FireSubjectChanged once the write
// transaction tx commits: FireSubjectEventAfterCommit for the whole subject.
func FireSubjectChangedAfterCommit(ctx context.Context, app *backpack.App, tx *gorm.DB, guard string, userID uint, kind string) {
FireSubjectEventAfterCommit(ctx, app, tx, pact.MCPSubjectChanged{Guard: guard, UserID: userID, Kind: kind})
}