ums

UserManagementDelegation — Aggregate Architecture

Bounded Context: Identity
Aggregate Root: UserManagementDelegation
Module: Ums.Domain.Identity.UserManagementDelegation
Schema: [delegation]
Status: Production
Functional Story: FS-14 — Delegate User Management Between Administrators
Epic: EP-06 (Post-MVP)


1. Aggregate Overview

Purpose

UserManagementDelegation allows an administrator (DelegatingAdmin) to transfer a controlled, time-bounded subset of their user management authority to another administrator (DelegatedAdmin). The delegated authority is restricted by scope type, allowed actions, and an optional approval gate. The aggregate enforces the no-elevation principle: a delegator can never grant more authority than they themselves hold.

Business Responsibility

Aggregate Root

UserManagementDelegation is the sole root. No child entities are owned; related objects (DelegatingAdmin, DelegatedAdmin) are referenced by ID only.

Diagram

classDiagram
    direction TB
    class UserManagementDelegation {
        <<AggregateRoot>>
        +Guid Id
        +Guid TenantId
        +Guid DelegatingAdminId
        +Guid DelegatedAdminId
        +DelegationScopeType ScopeType
        +Guid? ScopeId
        +AllowedActions AllowedActions
        +DateTimeOffset ValidFrom
        +DateTimeOffset ValidUntil
        +int? MaxDurationDays
        +bool RequiresApproval
        +Guid? ApprovalRequestId
        +DelegationStatus Status
        +DateTimeOffset? RevokedAt
        +Guid? RevokedBy
        +string? RevocationReason
        +UserCategory? RestrictedToUserCategory
        +Guid? RestrictedToOrganizationId
    }

State Machine

stateDiagram-v2
    [*] --> DRAFT : CreateDelegation
    DRAFT --> PENDING_APPROVAL : SubmitForApproval (RequiresApproval=true)
    DRAFT --> ACTIVE : Activate (RequiresApproval=false)
    PENDING_APPROVAL --> ACTIVE : ApproveDelegation
    PENDING_APPROVAL --> REJECTED : RejectDelegation
    ACTIVE --> REVOKED : RevokeDelegation (manual)
    ACTIVE --> EXPIRED : valid_until reached (Background Worker)
    ACTIVE --> COMPLETED : Period ends naturally
    REVOKED --> ARCHIVED : ArchiveDelegation
    EXPIRED --> ARCHIVED : ArchiveDelegation
    COMPLETED --> ARCHIVED : ArchiveDelegation
    REJECTED --> ARCHIVED : ArchiveDelegation
    ARCHIVED --> [*]
    note right of ACTIVE : IDelegationScopeValidator resolves\nonly ACTIVE delegations
    note right of ARCHIVED : Terminal — no transitions

Invariants and Consistency Rules

ID Rule Source
INV-DEL1 DelegatingAdmin cannot grant actions they do not hold — no privilege escalation FS-14 §6.1
INV-DEL2 DelegatingAdmin and DelegatedAdmin cannot be the same user FS-14 §6
INV-DEL3 ValidUntil > ValidFrom FS-14 §3
INV-DEL4 AllowedActions must be a non-empty subset of DelegatingAdmin’s actual authority FS-14 §6.2
INV-DEL5 Circular delegation is forbidden: B cannot delegate to A if A already delegates to B (direct) FS-14 §5.B
INV-DEL6 A DRAFT delegation is not visible to the delegated admin until ACTIVE EP-06 §2.2
INV-DEL7 A REVOKED or EXPIRED delegation cannot be re-activated; a new one must be created EP-06 §2.2
INV-DEL8 MaxDurationDays, if set, caps ValidUntil − ValidFrom; cannot be overridden at creation EP-06 §2.1
INV-DEL9 If RequiresApproval = true, delegation cannot transition to ACTIVE without an ApprovalRequestId with status = APPROVED EP-06 §2.1
INV-DEL10 ScopeId must be set when ScopeType is not TENANT design decision

Value Objects

Value Object Type Values
DelegationScopeType Enum TENANT · ORGANIZATION · DEPARTMENT · SYSTEM · TEAM
AllowedActions Value Object (list) CREATE_USER · BLOCK_USER · ASSIGN_PROFILE · RESET_PASSWORD · REVOKE_MFA
DelegationStatus Enum DRAFT · PENDING_APPROVAL · ACTIVE · REVOKED · EXPIRED · COMPLETED · REJECTED · ARCHIVED
ValidFrom DateTimeOffset Must be ≤ ValidUntil
ValidUntil DateTimeOffset Must be > ValidFrom
RevocationReason string? Required when Status → REVOKED
Reference Type Notes
DelegatingAdminId FK → UserAccount The admin granting authority
DelegatedAdminId FK → UserAccount The admin receiving authority
ApprovalRequestId FK → ApprovalRequest Set when RequiresApproval = true
ScopeId FK → tenant/org/dept/system/team entity Nullable; required when ScopeType ≠ TENANT

Domain Events

Event Trigger
DelegationCreatedEvent New delegation drafted { delegationId, delegatingAdminId, delegatedAdminId, scopeType, allowedActions }
DelegationSubmittedForApprovalEvent Sent to ApprovalWorkflow { delegationId, approvalRequestId }
DelegationActivatedEvent Delegation goes ACTIVE { delegationId, validFrom, validUntil }
DelegationRevokedEvent Manual revocation { delegationId, revokedBy, reason }
DelegationExpiredEvent Background worker expires { delegationId, expiredAt }
DelegationRejectedEvent Approval rejected { delegationId, rejectionReason }
DelegationArchivedEvent Terminal state { delegationId, previousStatus }

Commands / Use Cases

Command Actor Description
CreateDelegationCommand Delegating Admin Draft a new delegation with scope and allowed actions
SubmitDelegationForApprovalCommand Delegating Admin Route to approval workflow if RequiresApproval = true
ActivateDelegationCommand System / Approver Activate after approval or directly if no approval required
RevokeDelegationCommand Delegating Admin or Superior Admin Immediately deactivate with reason
ExpireDelegationCommand Background Worker Expire delegations where valid_until < now
CompleteDelegationCommand Background Worker Natural completion at period end
ArchiveDelegationCommand Background Worker Move terminal-state delegations to ARCHIVED

Repository / Service Boundaries


2. Object Model

UserManagementDelegation (Aggregate Root)
├── Props: UserManagementDelegationProps
│   ├── Id: IdValueObject
│   ├── TenantId: TenantId
│   ├── DelegatingAdminId: UserId
│   ├── DelegatedAdminId: UserId
│   ├── ScopeType: DelegationScopeType
│   ├── ScopeId?: ScopeId
│   ├── AllowedActions: AllowedActions           -- non-empty list VO
│   ├── ValidFrom: DateTimeOffset
│   ├── ValidUntil: DateTimeOffset
│   ├── MaxDurationDays?: int
│   ├── RequiresApproval: bool
│   ├── ApprovalRequestId?: ApprovalRequestId
│   ├── Status: DelegationStatus
│   ├── RevokedAt?: DateTimeOffset
│   ├── RevokedBy?: UserId
│   ├── RevocationReason?: string
│   ├── RestrictedToUserCategory?: UserCategory
│   ├── RestrictedToOrganizationId?: Guid
│   └── Audit: AuditValueObject
└── DomainEvents: UserManagementDelegationEventsManager

Main Attributes

Attribute Type Notes
Id Guid PK
TenantId Guid FK — RLS scope
DelegatingAdminId Guid FK → UserAccount
DelegatedAdminId Guid FK → UserAccount
ScopeType DelegationScopeType Boundary of authority
ScopeId Guid? Target org/dept/system entity; null when TENANT scope
AllowedActions string (JSON) ["CREATE_USER","ASSIGN_PROFILE",...]
ValidFrom DateTimeOffset Start of authority window
ValidUntil DateTimeOffset End of authority window
Status DelegationStatus Lifecycle state
RevocationReason string? Required on revocation

3. Sequence Diagrams

Create Delegation (No Approval Required)

sequenceDiagram
    participant A as Delegating Admin
    participant H as CreateDelegationHandler
    participant C as IDelegationAuthorityChecker
    participant D as UserManagementDelegation (AR)
    participant R as IDelegationRepository

    A->>H: CreateDelegationCommand(delegatedAdminId, scopeType, scopeId, allowedActions, validUntil)
    H->>C: HasAuthority(delegatingAdminId, allowedActions, scopeType, scopeId)
    C-->>H: true
    H->>C: IsCircular(delegatingAdminId, delegatedAdminId)
    C-->>H: false
    H->>D: UserManagementDelegation.Create(...)
    D->>D: INV-DEL1..10 guards
    D->>D: Status = DRAFT
    D->>D: Raise DelegationCreatedEvent
    H->>D: delegation.Activate()
    D->>D: Status = ACTIVE
    D->>D: Raise DelegationActivatedEvent
    H->>R: Add(delegation)
    H-->>A: delegationId

Create Delegation (Approval Required)

sequenceDiagram
    participant A as Delegating Admin
    participant H as CreateDelegationHandler
    participant D as UserManagementDelegation (AR)
    participant AW as ApprovalWorkflow (BC Approvals)
    participant AP as Approver

    A->>H: CreateDelegationCommand(..., RequiresApproval=true)
    H->>D: Create(...) → Status=DRAFT
    H->>D: SubmitForApproval(approvalRequestId)
    D->>D: Status = PENDING_APPROVAL
    D->>D: Raise DelegationSubmittedForApprovalEvent
    Note over AW: ApprovalWorkflow picks up event via Outbox
    AW->>AP: Notify approver
    AP->>AW: Approve
    AW-->>D: ApproveDelegationCommand(delegationId)
    D->>D: Status = ACTIVE
    D->>D: Raise DelegationActivatedEvent

Scope Validation at Command Execution

sequenceDiagram
    participant B as Delegated Admin
    participant H as RegisterUserHandler
    participant V as IDelegationScopeValidator
    participant U as UserAccount (AR)

    B->>H: RegisterUserCommand(actorId=B, tenantId, newUserData)
    H->>V: Validate(actorId=B, action=CREATE_USER, targetScopeId)
    V->>V: Find ACTIVE delegation where DelegatedAdminId=B\n AND action in AllowedActions\n AND scope covers targetScopeId
    V-->>H: ScopeValidationResult.Valid(delegationId)
    H->>U: UserAccount.Create(...)
    U->>U: Raise UserRegisteredEvent { createdByDelegationId }
    H-->>B: userId

Background Expiry

sequenceDiagram
    participant BW as Background Worker (Hourly)
    participant R as IDelegationRepository
    participant D as UserManagementDelegation (AR)

    BW->>R: FindActive(validUntil < now)
    R-->>BW: [delegation1, delegation2]
    loop Each expired delegation
        BW->>D: ExpireDelegationCommand(delegationId)
        D->>D: Status = EXPIRED
        D->>D: Raise DelegationExpiredEvent
    end
    BW->>R: SaveAll()

4. Entity / Relationship Model

Patrón dual self-join: USER_MANAGEMENT_DELEGATION referencia a USER_ACCOUNT dos veces con roles distintos. El diagrama usa los alias UA_GRANTOR y UA_GRANTEE para que Mermaid pueda trazar ambas líneas; en BD ambos alias mapean a la misma tabla [ums_identity].[UserAccounts]. La misma cuenta puede aparecer como grantor en N filas y como grantee en M filas simultáneamente. La anti-circularidad (A→B activo + B→A activo) se bloquea en aplicación mediante IDelegationAuthorityChecker (INV-DEL5); la auto-delegación se bloquea en BD con CHECK (DelegatingAdminId <> DelegatedAdminId) (INV-DEL2).

erDiagram
    %% UA_GRANTOR y UA_GRANTEE son el mismo UserAccounts — roles distintos
    UA_GRANTOR ||--o{ USER_MANAGEMENT_DELEGATION : "grants  (DelegatingAdminId)"
    UA_GRANTEE ||--o{ USER_MANAGEMENT_DELEGATION : "receives (DelegatedAdminId)"
    USER_MANAGEMENT_DELEGATION }o--o| APPROVAL_REQUEST : "requires_approval"

    UA_GRANTOR {
        uniqueidentifier Id PK
        nvarchar Email
        varchar Status "ACTIVE para poder delegar"
    }

    UA_GRANTEE {
        uniqueidentifier Id PK
        nvarchar Email
        varchar Status "ACTIVE para recibir delegación"
    }

    USER_MANAGEMENT_DELEGATION {
        uniqueidentifier Id PK
        uniqueidentifier TenantId FK "RLS"
        uniqueidentifier DelegatingAdminId FK "→ UserAccounts (grantor) · INV-DEL2 ≠ DelegatedAdminId"
        uniqueidentifier DelegatedAdminId FK "→ UserAccounts (grantee) · INV-DEL2 ≠ DelegatingAdminId"
        int ScopeTypeId "1=TENANT 2=ORGANIZATION 3=DEPARTMENT 4=SYSTEM 5=TEAM"
        uniqueidentifier ScopeId "Nullable — required when ScopeTypeId ≠ 1"
        nvarchar AllowedActionsJson "JSON: [CREATE_USER, BLOCK_USER, ...]"
        datetimeoffset ValidFrom
        datetimeoffset ValidUntil "CHECK ValidUntil > ValidFrom"
        int MaxDurationDays "Nullable"
        bit RequiresApproval
        uniqueidentifier ApprovalRequestId "Nullable FK"
        int StatusId "1=DRAFT 2=PENDING_APPROVAL 3=ACTIVE 4=REVOKED 5=EXPIRED 6=COMPLETED 7=REJECTED 8=ARCHIVED"
        datetimeoffset RevokedAt "Nullable"
        uniqueidentifier RevokedBy "Nullable FK → UserAccounts"
        nvarchar RevocationReason "Nullable — required on revocation"
        datetime2 CreatedAt
        nvarchar CreatedBy
        datetime2 UpdatedAt
        nvarchar UpdatedBy
    }

    APPROVAL_REQUEST {
        uniqueidentifier Id PK
        uniqueidentifier TenantId FK "RLS"
        varchar Status "PENDING-APPROVED-REJECTED"
    }

5. Bounded Context Model

flowchart TD
    subgraph Identity["Identity BC"]
        UMD[UserManagementDelegation AR]
        UA[UserAccount AR]
    end

    subgraph Approvals["Approvals BC"]
        AW[ApprovalWorkflow]
        AR[ApprovalRequest]
    end

    subgraph Authorization["Authorization BC"]
        PROF[Profile]
    end

    subgraph Audit["Audit BC"]
        AREC[AuditRecord]
    end

    subgraph Infrastructure["Application Services"]
        DSV[IDelegationScopeValidator]
        DAC[IDelegationAuthorityChecker]
        BWK[Background Worker — Expiry]
    end

    UMD -->|DelegationCreatedEvent| AREC
    UMD -->|DelegationActivatedEvent| AREC
    UMD -->|DelegationRevokedEvent| AREC
    UMD -->|DelegationSubmittedForApprovalEvent| AW
    AR  -->|ApproveDelegationCommand| UMD
    DSV -->|checks ACTIVE delegation| UMD
    DSV -->|gates RegisterUser / BlockUser / AssignProfile| UA
    DAC -->|validates authority + circular check| UMD
    BWK -->|ExpireDelegationCommand| UMD
    UA  -->|UserRegisteredEvent with delegationId| AREC
    PROF -->|ProfileAssignedEvent| AREC

6. API / Application Layer Contract

Commands

Command Input Output Notes
CreateDelegationCommand delegatingAdminId, delegatedAdminId, scopeType, scopeId?, allowedActions[], validFrom, validUntil, requiresApproval, restrictedToUserCategory? Guid delegationId INV-DEL1..10 validated
SubmitDelegationForApprovalCommand delegationId, approvalRequestId void Only from DRAFT
ActivateDelegationCommand delegationId void From DRAFT (no approval) or PENDING_APPROVAL (approved)
RevokeDelegationCommand delegationId, revokedBy, reason void From ACTIVE only
ExpireDelegationCommand delegationId void Background Worker only
CompleteDelegationCommand delegationId void Background Worker only
ArchiveDelegationCommand delegationId void Terminal states only

Queries

Query Returns
GetDelegationByIdQuery DelegationDetailDto
ListDelegationsGrantedByQuery PagedList<DelegationSummaryDto> — delegations this admin has given
ListDelegationsReceivedByQuery PagedList<DelegationSummaryDto> — delegations this admin holds
GetActiveDelegationForActionQuery ActiveDelegationDto? — used by IDelegationScopeValidator
ListActiveDelegationsByDelegatedAdminQuery List<DelegationSummaryDto> — all active authority of an admin

Application Service Contract

public interface IDelegationScopeValidator
{
    /// <summary>
    /// Returns a successful result if <paramref name="actorId"/> has an ACTIVE
    /// UserManagementDelegation that covers <paramref name="action"/>
    /// within the scope containing <paramref name="targetScopeId"/>.
    /// </summary>
    Task<Result<DelegationContext>> ValidateAsync(
        Guid actorId,
        DelegatedAction action,
        Guid tenantId,
        Guid? targetScopeId,
        CancellationToken ct);
}

7. Persistence Notes

Table

CREATE TABLE [delegation].[user_management_delegations] (
    [id]                          UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
    [root_tenant_id]              UNIQUEIDENTIFIER NOT NULL,
    [delegating_admin_id]         UNIQUEIDENTIFIER NOT NULL,
    [delegated_admin_id]          UNIQUEIDENTIFIER NOT NULL,
    [scope_type]                  VARCHAR(32)      NOT NULL,  -- TENANT | ORGANIZATION | DEPARTMENT | SYSTEM | TEAM
    [scope_id]                    UNIQUEIDENTIFIER NULL,
    [allowed_actions]             NVARCHAR(MAX)    NOT NULL,  -- JSON: ["CREATE_USER","ASSIGN_PROFILE",...]
    [valid_from]                  DATETIMEOFFSET   NOT NULL,
    [valid_until]                 DATETIMEOFFSET   NOT NULL,
    [max_duration_days]           INT              NULL,
    [requires_approval]           BIT              NOT NULL DEFAULT 0,
    [approval_request_id]         UNIQUEIDENTIFIER NULL,
    [status]                      VARCHAR(32)      NOT NULL DEFAULT 'DRAFT',
    [revoked_at]                  DATETIMEOFFSET   NULL,
    [revoked_by]                  UNIQUEIDENTIFIER NULL,
    [revocation_reason]           NVARCHAR(MAX)    NULL,
    [restricted_to_user_category] VARCHAR(32)      NULL,
    [restricted_to_org_id]        UNIQUEIDENTIFIER NULL,
    [created_by]                  UNIQUEIDENTIFIER NOT NULL,
    [created_at]                  DATETIME2        NOT NULL DEFAULT GETUTCDATE(),
    [updated_by]                  UNIQUEIDENTIFIER NULL,
    [updated_at]                  DATETIME2        NULL,

    CONSTRAINT pk_user_management_delegations
        PRIMARY KEY (id, root_tenant_id),
    CONSTRAINT fk_delegation_delegating_admin
        FOREIGN KEY (delegating_admin_id, root_tenant_id)
        REFERENCES [identity].[users](id, root_tenant_id),
    CONSTRAINT fk_delegation_delegated_admin
        FOREIGN KEY (delegated_admin_id, root_tenant_id)
        REFERENCES [identity].[users](id, root_tenant_id),
    CONSTRAINT fk_delegation_approval
        FOREIGN KEY (approval_request_id, root_tenant_id)
        REFERENCES [approval].[approval_requests](id, root_tenant_id),
    CONSTRAINT chk_valid_until_after_valid_from
        CHECK (valid_until > valid_from),
    CONSTRAINT chk_no_self_delegation
        CHECK (delegating_admin_id <> delegated_admin_id)
);

Indexes

Index Columns Purpose
IX_Delegation_DelegatedAdmin_Active delegated_admin_id, root_tenant_id WHERE status='ACTIVE' IDelegationScopeValidator hot path
IX_Delegation_DelegatingAdmin delegating_admin_id, root_tenant_id List delegations granted
IX_Delegation_Scope scope_type, scope_id, root_tenant_id Scope-based lookups
IX_Delegation_ValidUntil_Active valid_until, root_tenant_id WHERE status='ACTIVE' Background expiry worker
IX_Delegation_Status_Tenant status, root_tenant_id Operational dashboards

Transaction Boundary

UserManagementDelegation is saved in a single SaveChanges() call. The DelegationSubmittedForApprovalEvent is dispatched via Transactional Outbox to the Approvals BC — never via direct call.

RLS

root_tenant_id is set in SESSION_CONTEXT by DbConnectionInterceptor. The SQL Server RLS predicate on [delegation].[user_management_delegations] filters by root_tenant_id as secondary failsafe (Two-Layer RLS — ADR-TBD).


8. Security and Audit

Authorization Rules

Operation Required Role Notes
Create Delegation Tenant:Admin Subject to INV-DEL1 (no escalation)
Submit for Approval Tenant:Admin (delegating admin only) Only the creator can submit
Revoke Delegation Tenant:Admin (delegating admin) or Platform:SuperAdmin Superior admin can also revoke
View delegations received Tenant:Admin (delegated admin) Own delegations only
View delegations granted Tenant:Admin (delegating admin) Own delegations only
Expire / Archive Background Worker system identity Never user-facing

Audit Events

Security Invariants


← UserAccount Identity Domain Index Domain Aggregate Index FS-14