Files
Luis Mendes 64af753aab
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 19s
CI / security (pull_request) Successful in 32s
CI / typecheck (pull_request) Successful in 36s
CI / integration_tests (pull_request) Successful in 2m40s
CI / unit_tests (pull_request) Successful in 11m42s
CI / docker (pull_request) Successful in 40s
CI / benchmark-regression (pull_request) Successful in 21m35s
CI / coverage (pull_request) Successful in 45m10s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 18s
CI / build (push) Successful in 23s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 57s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 2m43s
CI / unit_tests (push) Successful in 12m13s
CI / benchmark-publish (push) Successful in 12m19s
CI / docker (push) Successful in 1m2s
CI / coverage (push) Successful in 1h26m6s
feat(security): add permission system
Implemented namespace/project/plan/skill permission model with role
bindings (owner/admin/editor/viewer) and default deny policy. Added
enforcement hooks at CLI/service boundaries that are server-only; local
mode returns permissive defaults. Includes role enums, permission check
service, role matrix documentation.

Includes Behave BDD scenarios, Robot integration tests, ASV benchmarks,
and reference documentation.

ISSUES CLOSED: #344
2026-02-28 20:21:18 +00:00

186 lines
5.4 KiB
Markdown

# Permission System
## Overview
CleverAgents implements a namespace/project/plan/skill permission model
with role bindings and a default-deny policy. An ``enforce_permission``
decorator is available for gating function calls; however, the decorator
is **not yet wired** into CLI or service call sites — that integration
is deferred to a future pass.
**Server-only enforcement**: in local mode the permission system returns
permissive defaults (all actions allowed). Server-side role evaluation
uses the role bindings registered on the ``PermissionService``.
## Roles
| Role | Description |
| -------- | ---------------------------------------- |
| `owner` | Full control — all actions on all scopes |
| `admin` | Administrative access — all actions |
| `editor` | Read, write, and execute |
| `viewer` | Read-only access |
OWNER and ADMIN currently grant the same set of actions. The
distinction is intentional — OWNER designates the resource creator or
namespace owner, while ADMIN is a delegated administrative role.
Future server-mode enforcement may differentiate them (e.g. ownership
transfer, namespace deletion) without requiring a schema change.
## Role → Action Matrix
| Action | Owner | Admin | Editor | Viewer |
| --------- | :---: | :---: | :----: | :----: |
| `read` | yes | yes | yes | yes |
| `write` | yes | yes | yes | — |
| `execute` | yes | yes | yes | — |
| `delete` | yes | yes | — | — |
| `admin` | yes | yes | — | — |
## Scopes
Permissions can be scoped to:
- **Namespace** — top-level organisational boundary
- **Project** — a project within a namespace
- **Plan** — an execution plan within a project
- **Skill** — a skill registered in the system
### Scope Hierarchy (Known Limitation)
Scopes are currently evaluated with **exact match** semantics.
A binding at `namespace` scope does not automatically cascade to
`project`, `plan`, or `skill` scopes within that namespace.
Hierarchical scope resolution (namespace > project > plan > skill)
is deferred to a future server-mode release.
## Role Bindings
A `RoleBinding` associates a *principal* (user or service identifier)
with a `PermissionRole` at a specific `PermissionScope` and scope ID.
Duplicate bindings (same principal, role, scope, and scope_id) are
rejected by `add_binding()`.
```python
from cleveragents.domain.models.core.permission import (
PermissionRole,
PermissionScope,
RoleBinding,
)
binding = RoleBinding(
principal="alice",
role=PermissionRole.EDITOR,
scope=PermissionScope.PROJECT,
scope_id="my-project",
)
```
## Permission Policy
The `PermissionPolicy` model controls the default-deny behaviour and
optional allow-overrides:
```python
from cleveragents.domain.models.core.permission import (
PermissionPolicy,
RoleBinding,
)
policy = PermissionPolicy(
default_deny=True,
allow_overrides=[binding],
)
```
## Permission Service
`PermissionService` is the entry point for checking permissions:
```python
from cleveragents.application.services.permission_service import (
PermissionService,
)
from cleveragents.domain.models.core.permission import (
PermissionAction,
PermissionScope,
)
svc = PermissionService()
check = svc.check_permission(
principal="alice",
action=PermissionAction.WRITE,
scope=PermissionScope.PROJECT,
scope_id="my-project",
)
print(check.result) # True in local mode
```
### Module-Level Default Service
A module-level default ``PermissionService`` is available so that
the ``enforce_permission`` decorator (and other call sites) share a
single service instance with pre-registered bindings:
```python
from cleveragents.application.services.permission_service import (
get_default_permission_service,
set_default_permission_service,
PermissionService,
)
# During application startup:
svc = PermissionService(bindings=[...])
set_default_permission_service(svc)
# Later — the decorator (and any code that calls
# get_default_permission_service()) will use this instance.
```
## Enforcement Decorator
The `enforce_permission` decorator is available for gating function
calls. It is not yet applied to any CLI or service entry point — call
sites will be wired in a future integration pass:
```python
from cleveragents.application.services.permission_service import (
enforce_permission,
)
from cleveragents.domain.models.core.permission import (
PermissionAction,
PermissionScope,
)
@enforce_permission(
PermissionAction.WRITE,
PermissionScope.PROJECT,
"my-project",
)
def create_resource():
...
```
In local mode the decorator is a no-op. In server mode it raises
`PermissionError` when the check fails. When no explicit `service`
is passed, the decorator uses the module-level default service (see
above).
## Local Mode Behaviour
When `CLEVERAGENTS_SERVER_MODE` is unset (or not `true`), the system
operates in **local mode**:
- `PermissionService.is_local_mode()` returns `True`
- All permission checks return `result=True`
- `get_role_bindings()` returns a synthetic OWNER binding
- The `DEFAULT_LOCAL_ROLE_MAPPING` maps `"*"` → `PermissionRole.OWNER`
## Audit Logging
Permission denials in server mode are logged as `permission_denied`
events via structlog at the WARNING level. Binding additions and
removals are logged as `binding_added` / `binding_removed` at INFO
level.