Skip to content

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:

kotlin
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:

kotlin
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:

kotlin
interface RolePermission {
    val id: RoleId                // role identifier
    val permissions: Set<PermissionId>  // assigned permission IDs
}

AppRolePermission ​

AppRolePermission combines an AppPermission with role assignments:

kotlin
interface AppRolePermission {
    val appPermission: AppPermission
    val rolePermissions: List<RolePermission>
    val rolePermissionIndexer: Map<RoleId, List<Permission>>
}

The rolePermissionIndexer performs the join between roles and permissions:

kotlin
val rolePermissionIndexer: Map<RoleId, List<Permission>>
    get() = rolePermissions.associate { rolePermission ->
        val permissions =
            if (rolePermission.permissions.contains(ALL_PERMISSION_ID)) {
                appPermission.permissionIndexer.values.toList()
            } else {
                rolePermission.permissions.mapNotNull { permissionId ->
                    appPermission.permissionIndexer[permissionId]
                }
            }
        rolePermission.id to permissions
    }

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:

  1. Checks appRolePermission.appPermission.condition.match(request, context) -- app-level gate
  2. Flattens all role-permission entries into a sequence
  3. 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:

kotlin
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 ​

mermaid
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
        +description: String
        +permissions: List~Permission~
        +spaced: Boolean
    }
    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 : rolePermissionIndexer

Role-Permission Indexing ​

mermaid
flowchart TD
    A["AppRolePermission.rolePermissionIndexer"] --> E["For each RolePermission"]
    E --> B{"Role permissions contain '*'?"}
    B -->|"yes"| C["Wildcard role gets ALL permissions"]
    C --> D["roleId -> all permissionIndexer.values"]
    B -->|"no"| 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 G fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style H fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Role-Based Authorization Sequence ​

mermaid
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)
    end

Permission ID Convention ​

Permission IDs follow the format appId.group.permission:

ExampleDescription
order.readRead orders
order.writeCreate/update orders
admin.users.deleteDelete users in admin app
*Wildcard - all permissions

This naming convention enables hierarchical organization and readable audit logs.

References ​

Licensed under the Apache License, Version 2.0.