// Package revokeallusersessions implements the trusted internal bulk revoke // use case for all sessions of one user. package revokeallusersessions import ( "context" "fmt" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/domain/devicesession" "galaxy/authsession/internal/ports" "galaxy/authsession/internal/service/shared" "galaxy/authsession/internal/telemetry" "go.uber.org/zap" ) // Input describes one trusted internal revoke-all-user-sessions request. type Input struct { // UserID identifies the owner whose sessions should be revoked. UserID string // ReasonCode stores the machine-readable revoke reason code. ReasonCode string // ActorType stores the machine-readable revoke actor type. ActorType string // ActorID stores the optional stable revoke actor identifier. ActorID string } // Result describes the frozen internal bulk revoke acknowledgement. type Result struct { // Outcome reports whether active sessions were revoked during the current // call. Outcome string // UserID identifies the user addressed by the operation. UserID string // AffectedSessionCount reports how many sessions changed state during the // current call. AffectedSessionCount int64 // AffectedDeviceSessionIDs lists every session identifier affected during // the current call. AffectedDeviceSessionIDs []string } // Service executes the trusted internal revoke-all-user-sessions use case. type Service struct { sessionStore ports.SessionStore userDirectory ports.UserDirectory publisher ports.GatewaySessionProjectionPublisher clock ports.Clock logger *zap.Logger telemetry *telemetry.Runtime } // New returns a revoke-all-user-sessions service wired to the required ports. func New(sessionStore ports.SessionStore, userDirectory ports.UserDirectory, publisher ports.GatewaySessionProjectionPublisher, clock ports.Clock) (*Service, error) { return NewWithObservability(sessionStore, userDirectory, publisher, clock, nil, nil) } // NewWithObservability returns a revoke-all-user-sessions service wired to the // required ports plus optional structured logging and telemetry dependencies. func NewWithObservability( sessionStore ports.SessionStore, userDirectory ports.UserDirectory, publisher ports.GatewaySessionProjectionPublisher, clock ports.Clock, logger *zap.Logger, telemetryRuntime *telemetry.Runtime, ) (*Service, error) { switch { case sessionStore == nil: return nil, fmt.Errorf("revokeallusersessions: session store must not be nil") case userDirectory == nil: return nil, fmt.Errorf("revokeallusersessions: user directory must not be nil") case publisher == nil: return nil, fmt.Errorf("revokeallusersessions: projection publisher must not be nil") case clock == nil: return nil, fmt.Errorf("revokeallusersessions: clock must not be nil") default: return &Service{ sessionStore: sessionStore, userDirectory: userDirectory, publisher: publisher, clock: clock, logger: namedLogger(logger, "revoke_all_user_sessions"), telemetry: telemetryRuntime, }, nil } } // Execute revokes all active sessions of one user and republishes revoked // gateway projections for every affected session. func (s *Service) Execute(ctx context.Context, input Input) (result Result, err error) { logFields := []zap.Field{ zap.String("component", "service"), zap.String("use_case", "revoke_all_user_sessions"), } defer func() { shared.LogServiceOutcome(s.logger, ctx, "revoke all user sessions completed", err, logFields...) }() userID, err := shared.ParseUserID(input.UserID) if err != nil { return Result{}, err } logFields = append(logFields, zap.String("user_id", userID.String())) revocation, err := shared.BuildRevocation(input.ReasonCode, input.ActorType, input.ActorID, s.clock.Now()) if err != nil { return Result{}, err } logFields = append(logFields, zap.String("reason_code", revocation.ReasonCode.String())) exists, err := s.userDirectory.ExistsByUserID(ctx, userID) if err != nil { return Result{}, shared.ServiceUnavailable(err) } s.telemetry.RecordUserDirectoryOutcome(ctx, "exists_by_user_id", boolOutcome(exists)) if !exists { return Result{}, shared.SubjectNotFound() } storeResult, err := s.sessionStore.RevokeAllByUserID(ctx, ports.RevokeUserSessionsInput{ UserID: userID, Revocation: revocation, }) if err != nil { return Result{}, shared.ServiceUnavailable(err) } if err := storeResult.Validate(); err != nil { return Result{}, shared.InternalError(err) } logFields = append(logFields, zap.String("outcome", string(storeResult.Outcome))) affectedDeviceSessionIDs := make([]string, 0, len(storeResult.Sessions)) for _, record := range storeResult.Sessions { if err := shared.PublishSessionProjectionWithTelemetry(ctx, s.publisher, record, s.telemetry, "revoke_all_user_sessions"); err != nil { return Result{}, err } affectedDeviceSessionIDs = append(affectedDeviceSessionIDs, record.ID.String()) } if storeResult.Outcome == ports.RevokeUserSessionsOutcomeNoActiveSessions { if err := s.republishCurrentRevokedSessions(ctx, userID); err != nil { return Result{}, err } } affectedSessionCount := int64(len(storeResult.Sessions)) if affectedSessionCount > 0 { s.telemetry.RecordSessionRevocations(ctx, "revoke_all_user_sessions", revocation.ReasonCode.String(), affectedSessionCount) } logFields = append(logFields, zap.Int64("affected_session_count", affectedSessionCount)) return Result{ Outcome: string(storeResult.Outcome), UserID: storeResult.UserID.String(), AffectedSessionCount: affectedSessionCount, AffectedDeviceSessionIDs: affectedDeviceSessionIDs, }, nil } func (s *Service) republishCurrentRevokedSessions(ctx context.Context, userID common.UserID) error { records, err := s.sessionStore.ListByUserID(ctx, userID) if err != nil { return shared.ServiceUnavailable(err) } for _, record := range records { if record.Status != devicesession.StatusRevoked { continue } if err := shared.PublishSessionProjectionWithTelemetry(ctx, s.publisher, record, s.telemetry, "revoke_all_user_sessions_repair"); err != nil { return err } } return nil } func boolOutcome(value bool) string { if value { return "exists" } return "missing" } func namedLogger(logger *zap.Logger, name string) *zap.Logger { if logger == nil { logger = zap.NewNop() } return logger.Named(name) }