Multi-Tenancy
CoSec treats multi-tenancy as a first-class concept in its security model. Tenants define horizontal boundaries between customers, and every authorization decision can be scoped to a specific tenant. This page covers the tenant model, tenant-aware principals, tenant-scoped policy evaluation, and how tenant context flows through the security pipeline.
Tenant Model
The Tenant interface (Tenant.kt:22) defines three tenant types using a simple ID-based classification:
classDiagram
class Tenant {
<<interface>>
+tenantId: String
+isPlatformTenant: Boolean
+isDefaultTenant: Boolean
+isUserTenant: Boolean
}
class TenantCapable {
<<interface>>
+tenant: Tenant
}
class SecurityContext {
<<interface>>
+principal: CoSecPrincipal
+attributes: MutableMap~String, Any~
}
class TenantPrincipal {
<<interface>>
}
class Policy {
<<interface>>
+id: PolicyId
+tenantId: String
+statements: List~Statement~
}
TenantCapable <|.. SecurityContext
TenantCapable <|.. TenantPrincipal
Tenant --> TenantCapable : wrapped by
Policy ..|> Tenant : extendsTenant Types and Constants
The Tenant companion object (Tenant.kt:50-68) defines the classification:
| Constant | Value | Meaning |
|---|---|---|
PLATFORM_TENANT_ID | "(platform)" | The root platform tenant with full administrative access |
DEFAULT_TENANT_ID | "(0)" | The default tenant for non-tenant-scoped operations |
Derived boolean properties provide convenient classification:
isPlatformTenant(line 35) -- True whentenantIdequalsPLATFORM_TENANT_ID. Platform tenants typically manage all other tenants.isDefaultTenant(line 41) -- True whentenantIdequalsDEFAULT_TENANT_ID. The default tenant is used when no explicit tenant is specified.isUserTenant(line 47) -- True when the tenant is neither platform nor default. This represents actual customer tenants.
flowchart TD
TENANT_ID["tenantId"] --> PLATFORM{"tenantId == '(platform)'"}
PLATFORM -->|Yes| PT["Platform Tenant<br>isPlatformTenant = true"]
PLATFORM -->|No| DEFAULT{"tenantId == '(0)'"}
DEFAULT -->|Yes| DT["Default Tenant<br>isDefaultTenant = true"]
DEFAULT -->|No| UT["User Tenant<br>isUserTenant = true"]
PT --> ACCESS_PT["Full administrative access"]
DT --> ACCESS_DT["Non-tenant-scoped operations"]
UT --> ACCESS_UT["Customer-scoped data isolation"]
style TENANT_ID fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style PLATFORM fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style PT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style DEFAULT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style DT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style UT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style ACCESS_PT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style ACCESS_DT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style ACCESS_UT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3TenantCapable and TenantPrincipal
TenantCapable (TenantCapable.kt:23) is a simple interface that exposes a tenant property. It is implemented by two key abstractions:
SecurityContext(SecurityContext.kt:34) -- The security context extendsTenantCapable, ensuring tenant information is available during authorization.TenantPrincipal(TenantPrincipal.kt:26) -- Extends bothCoSecPrincipalandTenantCapable, carrying tenant identity with the user's principal.
This design means tenant context is accessible through two paths during authorization:
context.tenant.tenantId-- from the security contextcontext.principal.tenant.tenantId-- from the tenant-aware principal (when available)
Tenant Context Flow
The tenant context flows through the entire security pipeline, from the incoming request to policy evaluation.
sequenceDiagram
autonumber
participant Client
participant Filter as ReactiveSecurityFilter
participant SCParser as SecurityContextParser
participant Authz as SimpleAuthorization
participant Repo as PolicyRepository
participant PolicyEval as Policy.verify()
Client->>Filter: HTTP Request (with tenant headers)
Filter->>Filter: requestParser.parse(exchange)
Note over Filter: Extract appId, spaceId from headers
Filter->>SCParser: parse(request)
Note over SCParser: Resolve tenant from token claims
SCParser-->>Filter: SecurityContext(tenant, principal)
Filter->>Authz: authorize(request, securityContext)
Authz->>Authz: verifyRoot(context)
Authz->>Authz: blacklistChecker.check()
Authz->>Repo: getGlobalPolicy()
Repo-->>Authz: List~Policy~ (tenant-scoped)
Authz->>PolicyEval: verify(request, securityContext)
Note over PolicyEval: Policy extends Tenant<br>Check tenantId match
PolicyEval-->>Authz: VerifyResult
Authz->>Repo: getPolicies(principal.policies)
Repo-->>Authz: List~Policy~ (tenant-scoped)
Authz->>Authz: evaluateDenyFirst()
Authz-->>Filter: Mono<AuthorizeResult>
Filter-->>Client: ResponseRequest-Level Tenant Identification
The Request interface (Request.kt:36) carries appId and spaceId properties that help identify the application and space (tenant partition) for the request. These are resolved from headers or query parameters:
override val appId: AppId
get() = getHeader(APP_ID_KEY).ifBlank { getQuery(APP_ID_KEY) }
override val spaceId: SpaceId
get() = getHeader(SPACE_ID_KEY).ifBlank { getQuery(SPACE_ID_KEY) }The spaceId often maps to a tenant identifier, allowing the authorization layer to scope role permissions to the correct tenant partition.
Policy-Level Tenant Scoping
The Policy interface (Policy.kt:45) extends Tenant, meaning every policy has a tenantId. This allows the PolicyRepository to fetch only policies relevant to the current tenant, and enables condition matchers to enforce tenant boundaries.
During authorization, SimpleAuthorization (SimpleAuthorization.kt:82-113) filters policies by their condition matchers, which can include tenant-aware checks like InTenant conditions.
Tenant Hierarchy
graph TD
PLATFORM["Platform Tenant<br>tenantId = '(platform)'"]
DEFAULT["Default Tenant<br>tenantId = '(0)'"]
T1["Tenant A<br>tenantId = 'org-001'"]
T2["Tenant B<br>tenantId = 'org-002'"]
T3["Tenant C<br>tenantId = 'org-003'"]
PLATFORM --> |manages| DEFAULT
PLATFORM --> |manages| T1
PLATFORM --> |manages| T2
PLATFORM --> |manages| T3
T1 --> |isUserTenant = true| U1["User Tenant<br>Customer-scoped data"]
T2 --> |isUserTenant = true| U2["User Tenant<br>Customer-scoped data"]
T3 --> |isUserTenant = true| U3["User Tenant<br>Customer-scoped data"]
style PLATFORM fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style DEFAULT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style T1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style T2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style T3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style U1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style U2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style U3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3The platform tenant is the administrative root. Platform administrators can define global policies that apply across all tenants, or create tenant-specific policies for individual customers. The SimpleAuthorization evaluation order ensures global policies (which may be platform-tenant-scoped) are evaluated before principal-specific policies (SimpleAuthorization.kt:156-178).
SPI Extension Points for Tenant Conditions
CoSec provides built-in ConditionMatcher implementations that enforce tenant boundaries:
InTenant-- Matches when the current security context belongs to a specified tenant. Used in policies that should only apply within a particular tenant scope.Authenticated-- Can be combined with tenant checks to enforce that only authenticated users within a specific tenant can access resources.
These condition matchers are registered via Java SPI (META-INF/services/me.ahoo.cosec.policy.condition.ConditionMatcherFactory), allowing custom tenant-aware conditions to be added without modifying the core framework.
References
- Tenant.kt -- Tenant interface with platform/default/user classification
- TenantCapable.kt -- TenantCapable interface
- TenantPrincipal.kt -- Principal with tenant awareness
- SecurityContext.kt -- Security context extending TenantCapable
- Policy.kt -- Policy extending Tenant
- Request.kt -- Request with appId and spaceId
- SimpleAuthorization.kt -- Authorization with tenant-scoped policy evaluation
Related Pages
- Security Model -- How policies and conditions are evaluated
- Reactive Design -- How tenant context flows through the reactive pipeline
- Module Dependency Graph -- Module structure overview