ums

PermissionTemplate — Aggregate Architecture

Bounded Context: Authorization
Aggregate Root: PermissionTemplate
Module: Ums.Domain.Authorization.Template
Status: Production


1. Aggregate Overview

Purpose

PermissionTemplate defines a reusable, versioned package of access rules for a specific Role within a SystemSuite. It acts as the authoritative blueprint for seeding Profile permissions when a new tenant is onboarded or a new role assignment is activated. Each item in the template targets a level of the SystemSuite hierarchy (suite → module → submodule → option) using an Exclusive Arc pattern, with a tri-state effect (IsAllowed, IsDenied, or neutral).

Business Responsibility

Aggregate Root

PermissionTemplate is the sole aggregate root. PermissionTemplateItem is an owned child entity managed exclusively through the parent aggregate — never accessed or mutated directly.

State Machine

stateDiagram-v2
    [*] --> Draft : Create
    Draft --> Published : Publish\n(requires ≥1 item)
    Published --> Deprecated : Deprecate
    Deprecated --> [*]
    note right of Draft : Items can be added,\nremoved, and mutated only\nwhen status = Draft
    note right of Published : Read-only — used for\nProfile seeding
    note right of Deprecated : Terminal — superseded\nby a newer template

Invariants and Consistency Rules

ID Rule Enforced by
INV-TPL1 (TenantId, RoleId, SystemSuiteId) combination must be unique per active template Repository uniqueness check
INV-TPL2 Items can only be added, removed, or mutated when Status = Draft TemplateNotDraft guard in every mutating method
INV-TPL3 A Published template cannot be re-drafted; a new template must be created TemplateNotPublished guard on Deprecate
INV-TPL4 (TargetType, TargetId, ActionId) must be unique within a template — no duplicate item mappings TemplateItemTargetAlreadyExists guard in AddItem
INV-TPL5 An item cannot have IsAllowed = true AND IsDenied = true simultaneously PermissionEffectValidator on every item
INV-TPL6 TargetId must never be null — Exclusive Arc requires a concrete target reference ExclusiveArcValidator on every item
Entity / VO Type Ownership Notes
PermissionTemplateItem Entity Owned One entry per (TargetType, TargetId, ActionId) triplet
TenantId Value Object FK ref Scopes the template to a tenant
RoleId Value Object FK ref The role this template packages rules for
SystemSuiteId Value Object FK ref The suite whose hierarchy the items reference
TemplateVersion Value Object Semver string ("0.1.0") — starts at Initial()
TemplateStatus Enumeration Draft(1) · Published(2) · Deprecated(3)
ExclusiveArcTarget Enumeration SystemSuite(1) · Module(2) · Submodule(3) · Option(4)
ActionId Value Object FK ref The specific action granted or denied
AuditValueObject Value Object CreatedBy/At, UpdatedBy/At on both AR and item

Domain Events

Event Trigger Payload
PermissionTemplateCreatedEvent Template drafted templateId, tenantId, roleId, systemSuiteId, version
PermissionTemplatePublishedEvent Template published templateId, version
PermissionTemplateMutatedEvent Item added, removed, or effect changed templateId, version
PermissionTemplateDeprecatedEvent Template deprecated templateId, version

Note: A single PermissionTemplateMutatedEvent covers all item-level changes (add, remove, allow/deny/neutral toggle, activate/deactivate). Downstream consumers react to any mutation on the template version, not to individual item operations.

Commands / Use Cases

Command Description Pre-condition
CreatePermissionTemplateCommand Draft a new template for (tenantId, roleId, systemSuiteId) No active template for that triplet
PublishPermissionTemplateCommand Transition Draft → Published Status = Draft
AddTemplateItemCommand Add an item (targetType, targetId, actionId, isAllowed, isDenied) Status = Draft; no duplicate triplet
SetItemAllowCommand Set IsAllowed=true, IsDenied=false Status = Draft
SetItemDenyCommand Set IsAllowed=false, IsDenied=true Status = Draft
SetItemNeutralCommand Set IsAllowed=false, IsDenied=false (inherit from parent scope) Status = Draft
ActivateItemCommand Set IsActive=true Status = Draft
DeactivateItemCommand Set IsActive=false Status = Draft
RemoveTemplateItemCommand Remove an item from the template Status = Draft
DeprecatePermissionTemplateCommand Transition Published → Deprecated Status = Published

Repository / Service Boundaries


2. Object Model

PermissionTemplate (Aggregate Root)
├── Props: PermissionTemplateProps
│   ├── Id: IdValueObject
│   ├── TenantId: TenantId
│   ├── RoleId: RoleId
│   ├── SystemSuiteId: SystemSuiteId
│   ├── Version: TemplateVersion          -- semver, starts at "0.1.0"
│   ├── Status: TemplateStatus            -- Draft | Published | Deprecated
│   └── Audit: AuditValueObject
└── Children
    └── IReadOnlyCollection<PermissionTemplateItem>
        └── Props: PermissionTemplateItemProps
            ├── Id: IdValueObject
            ├── TemplateId: TemplateId
            ├── TargetType: ExclusiveArcTarget   -- SystemSuite | Module | Submodule | Option
            ├── TargetId: IdValueObject           -- FK to the target entity
            ├── ActionId: ActionId                -- FK to the action catalog
            ├── IsAllowed: bool                   -- tri-state: IsAllowed=true,IsDenied=false → Allow
            ├── IsDenied: bool                    -- tri-state: IsAllowed=false,IsDenied=true → Deny
            ├── IsActive: bool                    -- inactive items are skipped during provisioning
            └── Audit: AuditValueObject

Main Attributes

Attribute Entity Type Notes
Id PermissionTemplate Guid PK
TenantId PermissionTemplate Guid FK — scopes template
RoleId PermissionTemplate Guid FK — role being templated
SystemSuiteId PermissionTemplate Guid FK — suite context
Version PermissionTemplate string Semver "major.minor.patch"
Status PermissionTemplate TemplateStatus Draft(1)·Published(2)·Deprecated(3)
Id PermissionTemplateItem Guid PK
TemplateId PermissionTemplateItem Guid FK → PermissionTemplate
TargetType PermissionTemplateItem ExclusiveArcTarget SystemSuite(1)·Module(2)·Submodule(3)·Option(4)
TargetId PermissionTemplateItem Guid FK to the target entity (arc target)
ActionId PermissionTemplateItem Guid FK to Action catalog
IsAllowed PermissionTemplateItem bool Explicit allow — mutually exclusive with IsDenied
IsDenied PermissionTemplateItem bool Explicit deny — mutually exclusive with IsAllowed
IsActive PermissionTemplateItem bool Inactive items skipped on profile seeding

3. Class Diagram

classDiagram
    direction TB

    class PermissionTemplate {
        <<AggregateRoot>>
        +Guid Id
        +Guid TenantId
        +Guid RoleId
        +Guid SystemSuiteId
        +TemplateVersion Version
        +TemplateStatus Status
        +AuditValueObject Audit
        +IReadOnlyCollection~PermissionTemplateItem~ Items
        +Create(tenantId, roleId, systemSuiteId, createdBy) Result
        +Publish(updatedBy) Result
        +Deprecate(updatedBy) Result
        +AddItem(targetType, targetId, actionId, isAllowed, isDenied, createdBy) Result
        +SetItemAllow(itemId, updatedBy) Result
        +SetItemDeny(itemId, updatedBy) Result
        +SetItemNeutral(itemId, updatedBy) Result
        +ActivateItem(itemId, updatedBy) Result
        +DeactivateItem(itemId, updatedBy) Result
        +RemoveItem(itemId, updatedBy) Result
    }

    class PermissionTemplateItem {
        <<OwnedEntity>>
        +Guid Id
        +Guid TemplateId
        +ExclusiveArcTarget TargetType
        +Guid TargetId
        +Guid ActionId
        +bool IsAllowed
        +bool IsDenied
        +bool IsActive
        +AuditValueObject Audit
        +SetAllow(updatedBy) Result
        +SetDeny(updatedBy) Result
        +SetNeutral(updatedBy) Result
        +Activate(updatedBy) Result
        +Deactivate(updatedBy) Result
    }

    class TemplateStatus {
        <<Enumeration>>
        Draft = 1
        Published = 2
        Deprecated = 3
    }

    class ExclusiveArcTarget {
        <<Enumeration>>
        SystemSuite = 1
        Module = 2
        Submodule = 3
        Option = 4
    }

    PermissionTemplate "1" *-- "0..*" PermissionTemplateItem : owns
    PermissionTemplate --> TemplateStatus
    PermissionTemplateItem --> ExclusiveArcTarget

4. Sequence Diagrams

Create Template & Publish Flow

sequenceDiagram
    participant A as Admin
    participant H as CreatePermissionTemplateHandler
    participant U as ITemplateUniquenessChecker
    participant T as PermissionTemplate (AR)
    participant R as IPermissionTemplateRepository

    A->>H: CreatePermissionTemplateCommand(tenantId, roleId, systemSuiteId)
    H->>U: IsUnique(tenantId, roleId, systemSuiteId)
    U-->>H: true
    H->>T: PermissionTemplate.Create(...)
    T->>T: Status = Draft · Version = "0.1.0"
    T->>T: Raise PermissionTemplateCreatedEvent
    H->>R: Add(template)
    H-->>A: templateId

    Note over A,R: Admin adds items...

    A->>H: PublishPermissionTemplateCommand(templateId)
    H->>R: GetById(templateId)
    R-->>H: PermissionTemplate (Draft)
    H->>T: template.Publish(actorId)
    T->>T: Guard: Status must be Draft
    T->>T: Status = Published
    T->>T: Raise PermissionTemplatePublishedEvent
    H->>R: Update(template)
    H-->>A: void

Add Item Flow (Exclusive Arc)

sequenceDiagram
    participant A as Admin
    participant H as AddTemplateItemHandler
    participant T as PermissionTemplate (AR)
    participant R as IPermissionTemplateRepository

    A->>H: AddTemplateItemCommand(templateId, targetType, targetId, actionId, isAllowed, isDenied)
    H->>R: GetById(templateId)
    R-->>H: PermissionTemplate (Draft)
    H->>T: template.AddItem(targetType, targetId, actionId, isAllowed, isDenied, actorId)
    T->>T: Guard: Status = Draft
    T->>T: Guard: no duplicate (TargetType, TargetId, ActionId)
    T->>T: ExclusiveArcValidator — TargetId not null
    T->>T: PermissionEffectValidator — not (IsAllowed ∧ IsDenied)
    T->>T: Raise PermissionTemplateMutatedEvent
    H->>R: Update(template)
    H-->>A: itemId

Permission Effect Override Flow

sequenceDiagram
    participant A as Admin
    participant H as SetItemDenyHandler
    participant T as PermissionTemplate (AR)

    A->>H: SetItemDenyCommand(templateId, itemId)
    H->>R: GetById(templateId)
    R-->>H: PermissionTemplate (Draft)
    H->>T: template.SetItemDeny(itemId, actorId)
    T->>T: Guard: Status = Draft
    T->>T: item.IsDenied = true · item.IsAllowed = false
    T->>T: Raise PermissionTemplateMutatedEvent
    H->>R: Update(template)
    H-->>A: void

5. Entity / Relationship Model

Exclusive Arc: PERMISSION_TEMPLATE_ITEM.TargetId is a polymorphic FK — it can reference a SystemSuite, Module, Submodule, or Option depending on TargetTypeId. This is the Exclusive Arc pattern: only one of the four possible references is valid per row, identified by TargetTypeId. No SQL foreign-key constraint spans all four targets; referential integrity is enforced at the application layer.

erDiagram
    PERMISSION_TEMPLATE ||--o{ PERMISSION_TEMPLATE_ITEM : "contains"
    PERMISSION_TEMPLATE }o--|| TENANT                   : "scoped_to"
    PERMISSION_TEMPLATE }o--|| ROLE                     : "packages_for"
    PERMISSION_TEMPLATE }o--|| SYSTEM_SUITE             : "covers"
    PERMISSION_TEMPLATE_ITEM }o--|| ACTION              : "targets_action"

    PERMISSION_TEMPLATE {
        uniqueidentifier Id PK
        uniqueidentifier TenantId FK "RLS scope"
        uniqueidentifier RoleId FK "role being templated"
        uniqueidentifier SystemSuiteId FK "suite context"
        nvarchar Version "semver — 0.1.0"
        int StatusId "1=Draft 2=Published 3=Deprecated"
        nvarchar CreatedBy
        datetime2 CreatedAtUtc
        nvarchar UpdatedBy "Nullable"
        datetime2 UpdatedAtUtc "Nullable"
        nvarchar AuditTimeSpan
    }

    PERMISSION_TEMPLATE_ITEM {
        uniqueidentifier Id PK
        uniqueidentifier TemplateId FK
        int TargetTypeId "1=SystemSuite 2=Module 3=Submodule 4=Option"
        uniqueidentifier TargetId "Exclusive Arc — points to entity of TargetTypeId"
        uniqueidentifier ActionId FK "→ Action catalog"
        bit IsAllowed "true = explicit Allow"
        bit IsDenied "true = explicit Deny — CHECK NOT (IsAllowed AND IsDenied)"
        bit IsActive "false = skip during Profile seeding"
        nvarchar CreatedBy
        datetime2 CreatedAtUtc
        nvarchar UpdatedBy "Nullable"
        datetime2 UpdatedAtUtc "Nullable"
        nvarchar AuditTimeSpan
    }

    ACTION {
        uniqueidentifier Id PK
        nvarchar Code "unique action code"
        nvarchar Name
    }

6. Bounded Context Model

flowchart TD
    subgraph Authorization["Authorization BC"]
        PT[PermissionTemplate AR]
        PTI[PermissionTemplateItem]
        PROF[Profile AR]
        PP[ProfilePermission]
        PT --> PTI
        PROF --> PP
    end

    subgraph Identity["Identity BC"]
        TN[Tenant AR]
        UA[UserAccount AR]
    end

    subgraph SystemSuite_BC["SystemSuite BC"]
        SS[SystemSuite AR]
        ACT[Action]
    end

    PT -->|TenantId FK| TN
    PT -->|SystemSuiteId FK| SS
    PTI -->|ActionId FK| ACT

    PT -->|PermissionTemplateCreatedEvent| PROF
    PT -->|PermissionTemplatePublishedEvent\nseeds ProfilePermissions| PROF
    UA -->|UserRegisteredEvent → triggers\nProfile provisioning| PROF

7. API / Application Layer Contract

Commands

Command Input Output Notes
CreatePermissionTemplateCommand tenantId, roleId, systemSuiteId Guid templateId Starts at Draft · Version “0.1.0”
PublishPermissionTemplateCommand templateId void Status must be Draft
DeprecatePermissionTemplateCommand templateId void Status must be Published
AddTemplateItemCommand templateId, targetType, targetId, actionId, isAllowed, isDenied Guid itemId Status must be Draft
SetItemAllowCommand templateId, itemId void Status must be Draft
SetItemDenyCommand templateId, itemId void Status must be Draft
SetItemNeutralCommand templateId, itemId void Status must be Draft
ActivateItemCommand templateId, itemId void Status must be Draft
DeactivateItemCommand templateId, itemId void Status must be Draft
RemoveTemplateItemCommand templateId, itemId void Status must be Draft

Queries

Query Returns
GetAllPermissionTemplatesQuery PagedResult<PermissionTemplateDto> — filterable by tenantId, status
GetPermissionTemplateByIdQuery PermissionTemplateDetailDto with items
GetTemplatesByRoleQuery List<PermissionTemplateSummaryDto>

DTO Contract

// Application layer read model
public sealed record PermissionTemplateDto(
    Guid TemplateId,
    Guid TenantId,
    Guid RoleId,
    Guid SystemSuiteId,
    string Version,
    string Status);

8. Persistence Notes

Indexes

Index Columns Type Purpose
UQ_PermissionTemplate_Tenant_Role_Suite TenantId, RoleId, SystemSuiteId Unique (filtered StatusId ≠ 3) INV-TPL1 — no duplicate active template
IX_PermissionTemplate_TenantId TenantId Non-unique Tenant-scoped listing
IX_PermissionTemplate_StatusId StatusId Non-unique Status filter
UQ_PermissionTemplateItem_Target_Action TemplateId, TargetTypeId, TargetId, ActionId Unique INV-TPL4 — no duplicate item triplet
IX_PermissionTemplateItem_TemplateId TemplateId Non-unique Load all items for a template

Transaction Boundary

PermissionTemplate and all its PermissionTemplateItem children are saved in a single SaveChanges() call. The PermissionTemplatePublishedEvent is dispatched via Transactional Outbox to trigger downstream Profile seeding.

Security

Operation Required Role
Create / mutate / publish global template Platform:Admin
Create / mutate / publish tenant-scoped template Tenant:Admin
Deprecate any template Platform:Admin
Read templates Tenant:Admin, Platform:Admin

← Authorization Index Domain Aggregate Index