UMS-specific: No Evolith upstream. Tenant portal authorization boundaries and scope policies are intrinsic to UMS’s multi-tenant SaaS architecture and not generalizable to other satellite products.
Status: Accepted
Date: 2026-06-02
Decision Owner: Architecture
Related:
UMS must support two different authorization responsibilities that are easy to confuse if they are implemented with the same mechanism:
| Flow | Purpose | Governing rule |
|---|---|---|
| Portal management | A tenant user signs into the UMS portal to view or manage their own allowed data | Must be authorized by tenant scope, roles, permissions, and Tenant.IsManagementOwner |
| Public external API | A downstream client authenticates against the public auth API | Must honor the tenant’s configured auth method and the auth graph |
Historically, cross-cutting AOP was a tempting place to enforce access checks because it is already attached to command handlers. That would be the wrong boundary for a security decision. Authorization is business-critical, must remain explicit, and must be easy to inspect in tests and code reviews.
The portal flow therefore needs an internal management boundary that is separate from the external API authentication flow. The management boundary must be tenant-aware, read the tenant ownership flag from the domain model, and prevent any tenant from operating outside its scope.
UMS will treat portal management authorization as an explicit application policy, not as an AOP concern.
| Decision Area | Choice |
|---|---|
| Portal scope | Introduce AuthAccessScope.PortalManagement for portal access and AuthAccessScope.ExternalApi for the public auth API. |
| Internal management rule | Use ITenantScopePolicy to validate tenant ownership and scope before executing mutating portal commands. |
| Tenant ownership | Use Tenant.IsManagementOwner as the authoritative flag that enables internal management actions for a tenant. |
| External API auth | Keep IAuthMethodResolver.ResolveAsync(tenantId, scope) as the explicit resolver for the public authentication API. |
| AOP usage | Reserve AOP for logging, tracing, audit enrichment, and other cross-cutting concerns. Never use it as the primary authorization boundary. |
| Query scope | Allow query handlers to use the policy for scope resolution, but keep write authorization explicit at the command boundary. |
flowchart TB
subgraph Presentation["Presentation"]
P1["AuthEndpoints"]
P2["ClientAuthEndpoints"]
P3["Management endpoints"]
P4["GraphQL / Middleware"]
end
subgraph Application["Application"]
A1["AuthMethodResolverService"]
A2["TenantScopePolicy"]
A3["Command handlers"]
A4["Query handlers"]
A5["AOP logging aspects"]
end
subgraph Domain["Domain"]
D1["Tenant"]
D2["AuthAccessScope"]
D3["AuthorizationGraph"]
D4["Business invariants"]
end
subgraph Infrastructure["Infrastructure"]
I1["TenantRepository"]
I2["Configuration providers"]
I3["Seeders / migrations"]
end
P1 --> A1
P2 --> A1
P3 --> A2
P3 --> A3
P4 --> A4
A1 --> D2
A2 --> D1
A3 --> D1
A4 --> D1
A1 --> I2
A2 --> I1
I1 --> D1
I2 --> A1
A5 -. cross-cutting only .-> A3
A5 -. cross-cutting only .-> A4
A3 --> D3
sequenceDiagram
participant User as Portal user
participant Portal as UMS portal
participant Handler as Command handler
participant Policy as TenantScopePolicy
participant Tenant as Tenant aggregate
User->>Portal: Sign in to the UMS portal
Portal->>Handler: AuthenticateUserCommand(PortalManagement)
Handler-->>Portal: Session + graph
User->>Portal: Create or update tenant-owned resource
Portal->>Handler: Mutating command
Handler->>Policy: EnsureManagementOwnerScopeAsync(tenantId)
Policy->>Tenant: Load tenant and inspect IsManagementOwner
Tenant-->>Policy: true / false
Policy-->>Handler: Allowed / denied
Handler-->>Portal: Result
| Alternative | Decision | Reason |
|---|---|---|
| Enforce tenant access in AOP aspects | Rejected | Security rules would become implicit and harder to audit. |
| Enforce tenant access only in middleware | Rejected | Middleware is too early and too coarse for many business rules. |
| Merge portal management and external API auth into one flow | Rejected | The two use cases have different trust boundaries and different business rules. |
Store management access outside Tenant |
Rejected | Tenant ownership must be the source of truth for internal management scope. |
| Area | Guidance |
|---|---|
| Commands | Mutating portal commands must validate ITenantScopePolicy before any write occurs. |
| Queries | Query handlers may use the policy to resolve the visible scope, but never to bypass authorization. |
| Auth | Use AuthAccessScope.PortalManagement for portal login and AuthAccessScope.ExternalApi for the public auth API. |
| Domain | Keep Tenant.IsManagementOwner as the source of truth for internal management capability. |
| AOP | Use LoggerAspect and related aspects only for instrumentation, not for granting access. |
| Traceability | Link this ADR from functional stories and domain documentation that talk about tenant portal management. |