Permissions and Roles
CoSec implements role-based access control (RBAC) through a layered permission model. Permissions are app-scoped, organized into groups, and assigned to roles. The AppRolePermission interface ties it all together by mapping roles to their permitted actions within an application.
Permission Model
Permission
Permission extends Statement with identity and description:
interface Permission : Statement {
val id: PermissionId // format: "appId.group.permission"
val description: String
}Because Permission extends Statement, each permission carries its own effect (ALLOW/DENY), action matcher, and condition matcher. This means individual permissions can target specific API endpoints with their own conditions.
PermissionGroup
Permissions are organized into groups (e.g., "read", "write", "admin") via PermissionGroup, which holds a list of Permission instances.
AppPermission
AppPermission represents the full permission set for a single application:
interface AppPermission {
val id: AppId // application identifier
val condition: ConditionMatcher // app-level condition gate
val groups: List<PermissionGroup> // grouped permissions
val permissionIndexer: Map<PermissionId, Permission> // computed index
}The permissionIndexer is a computed property that flattens all groups into a single map keyed by permission ID, enabling O(1) lookup.
Role-Based Permissions
RolePermission
RolePermission maps a role to a set of permission IDs:
interface RolePermission {
val id: RoleId // role identifier
val permissions: Set<PermissionId> // assigned permission IDs
}AppRolePermission
AppRolePermission combines an AppPermission with role assignments:
interface AppRolePermission {
val appPermission: AppPermission
val rolePermissions: List<RolePermission>
val rolePermissionIndexer: Map<RoleId, List<Permission>>
}The rolePermissionIndexer performs the join between roles and permissions:
val rolePermissionIndexer: Map<RoleId, List<Permission>>
get() {
// Check for wildcard first
rolePermissions.forEach {
if (it.permissions.contains(ALL_PERMISSION_ID)) {
return mapOf(it.id to appPermission.permissionIndexer.values.toList())
}
}
// Normal mapping
return rolePermissions.associate {
it.id to it.permissions.mapNotNull { permId ->
appPermission.permissionIndexer[permId]
}
}
}Wildcard Permission
The constant ALL_PERMISSION_ID = "*" grants all permissions in the application to a role. When any RolePermission has "*" in its permissions set, that role's indexer entry includes every permission from the AppPermission.
Role-Based Evaluation in Authorization
During authorization (see Authorization Flow), SimpleAuthorization.verifyAppRolePermission:
- Checks
appRolePermission.appPermission.condition.match(request, context)-- app-level gate - Flattens all role-permission entries into a sequence
- Applies the deny-first algorithm to evaluate permissions
Each role maps to its set of resolved Permission objects, which are themselves Statement instances with their own action and condition matchers.
Load-Time Validation
DefaultAppPermissionEvaluator
DefaultAppPermissionEvaluator validates AppPermission at load time:
object DefaultAppPermissionEvaluator : AppPermissionEvaluator {
override fun evaluate(appPermission: AppPermission) {
val evaluateRequest = EvaluateRequest()
val mockContext = SimpleSecurityContext(SimpleTenantPrincipal.ANONYMOUS)
// Validate app-level condition
safeEvaluate { appPermission.condition.match(evaluateRequest, mockContext) }
// Validate each permission
appPermission.permissionIndexer.values.forEach { permission ->
safeEvaluate { permission.condition.match(evaluateRequest, mockContext) }
safeEvaluate { permission.action.match(evaluateRequest, mockContext) }
safeEvaluate { permission.verify(evaluateRequest, mockContext) }
}
}
}This mirrors the DefaultPolicyEvaluator pattern for policies, catching misconfigurations at deployment time.
Architecture Diagrams
Permission Model Class Diagram
classDiagram
direction TB
class Statement {
<<interface>>
+name: String
+effect: Effect
+action: ActionMatcher
+condition: ConditionMatcher
+verify(request, context): VerifyResult
}
class Permission {
<<interface>>
+id: PermissionId
+description: String
}
class PermissionGroup {
<<interface>>
+name: String
+permissions: List~Permission~
}
class AppPermission {
<<interface>>
+id: AppId
+condition: ConditionMatcher
+groups: List~PermissionGroup~
+permissionIndexer: Map~PermissionId Permission~
}
class RolePermission {
<<interface>>
+id: RoleId
+permissions: Set~PermissionId~
}
class AppRolePermission {
<<interface>>
+appPermission: AppPermission
+rolePermissions: List~RolePermission~
+rolePermissionIndexer: Map~RoleId List Permission~~
}
Statement <|-- Permission
PermissionGroup --> Permission : contains
AppPermission --> PermissionGroup : groups
AppPermission --> Permission : permissionIndexer
RolePermission --> PermissionId : references
AppRolePermission --> AppPermission : appPermission
AppRolePermission --> RolePermission : rolePermissions
AppRolePermission --> Permission : rolePermissionIndexerRole-Permission Indexing
flowchart TD
A["AppRolePermission.rolePermissionIndexer"] --> B{"Any RolePermission has '*'?"}
B -->|"yes"| C["Wildcard role gets ALL permissions"]
C --> D["roleId -> all permissionIndexer.values"]
B -->|"no"| E["Normal mapping"]
E --> F["For each RolePermission"]
F --> G["Map permissions set to Permission objects via permissionIndexer"]
G --> H["roleId -> List<Permission>"]
style A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style C fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style D fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style E fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style F fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style G fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style H fill:#2d333b,stroke:#6d5dfc,color:#e6edf3Role-Based Authorization Sequence
sequenceDiagram
autonumber
participant Auth as SimpleAuthorization
participant Repo as AppRolePermissionRepository
participant ARP as AppRolePermission
participant Eval as evaluateDenyFirst
Auth->>Repo: getAppRolePermission(appId, spaceId, roles)
Repo-->>Auth: AppRolePermission
Auth->>ARP: appPermission.condition.match(request, context)
alt Condition fails
ARP-->>Auth: null (skip)
else Condition passes
ARP->>ARP: build rolePermissionIndexer
ARP->>Eval: evaluateDenyFirst(rolePermissionEntries)
Note over Eval: Phase 1: DENY permissions
loop each DENY permission
Eval->>Eval: permission.verify(request, context)
alt matches DENY
Eval-->>Auth: RoleVerifyContext
end
end
Note over Eval: Phase 2: ALLOW permissions
loop each ALLOW permission
Eval->>Eval: permission.verify(request, context)
alt matches ALLOW
Eval-->>Auth: RoleVerifyContext
end
end
Eval-->>Auth: null (no match)
endPermission ID Convention
Permission IDs follow the format appId.group.permission:
| Example | Description |
|---|---|
order.read | Read orders |
order.write | Create/update orders |
admin.users.delete | Delete users in admin app |
* | Wildcard - all permissions |
This naming convention enables hierarchical organization and readable audit logs.
References
- Permission.kt:33 - Permission interface extending Statement
- AppPermission.kt:19 - Application-level permission container
- RolePermission.kt:28 - Role-to-permission mapping
- AppRolePermission.kt:16 - Combined app + role permission with indexer
- DefaultAppPermissionEvaluator.kt:23 - Load-time validation
Related Pages
- Authorization Flow - How role-based permissions fit in the authorization pipeline
- Policy Evaluation - Policy-level evaluation (parallel to permission evaluation)
- Action Matchers - Action patterns used in permissions
- Condition Matchers - Conditions used in permissions