v0.5.0 · Role-Based Access Control (RBAC) system with AI governance, space-scoped authorization, and audit logging.
Numen includes a full RBAC system that lets you manage team access with granular permissions, budget limits on AI generation, and an immutable audit log of all sensitive actions.
- No external dependencies — Numen's own RBAC, not a third-party package
- Permissions as strings — flat, greppable, composable (e.g.,
content.publish,ai.generate) - Space-scoped roles — a user can be Editor in Space A and Author in Space B
- API token scoping — personal access tokens and API keys inherit a subset of user permissions
- AI governance — budget limits, model access restrictions, and per-token scoping
- Audit logs — immutable records of all sensitive actions (content publish, role assignment, etc.)
- Built-in roles — Admin, Editor, Author, Viewer with sensible defaults (all editable)
Every space includes four system roles, seeded on first migration. They're editable but not deletable.
| Role | Key Permissions | AI Limits | Use Case |
|---|---|---|---|
| Admin | * (wildcard — everything) |
Unlimited | Full access; manage users, roles, settings |
| Editor | content.*, pipeline.*, media.*, ai.generate, settings.personas |
100 gen/day, all models except Opus | Manage content, run pipelines, approve content |
| Author | content.create/read/update, pipeline.run, media.upload, ai.generate |
20 gen/day, Haiku only | Write and submit content for review |
| Viewer | content.read, media.read |
No AI access | View-only access to published content |
New users start with no role — you must explicitly assign one.
Permissions follow a domain.action or domain.sub.action pattern. Use the GET /api/v1/permissions endpoint to fetch the full list.
| Permission | Description |
|---|---|
content.create |
Create new content entries |
content.read |
View all content (draft + published) |
content.update |
Edit existing content |
content.delete |
Delete content |
content.publish |
Publish / unpublish content |
content.restore |
Restore deleted content |
| Permission | Description |
|---|---|
pipeline.run |
Trigger pipeline execution |
pipeline.approve |
Approve pending runs for publication |
pipeline.reject |
Reject pipeline output |
| Permission | Description |
|---|---|
users.manage |
Invite, edit, deactivate users |
users.roles.assign |
Assign and revoke roles |
users.invite |
Invite new users |
users.delete |
Delete user accounts |
| Permission | Description |
|---|---|
roles.manage |
Create, edit, delete custom roles |
| Permission | Description |
|---|---|
spaces.manage |
Create and configure spaces |
spaces.delete |
Delete spaces |
| Permission | Description |
|---|---|
audit.view |
View audit logs |
settings.general |
Modify general settings |
settings.api_tokens |
Manage API tokens |
| Permission | Description |
|---|---|
ai.generate |
Trigger AI content generation |
component.manage |
Register and update custom component types |
persona.view |
View persona configurations |
*grants everythingcontent.*grants all content permissions (content.create,content.publish, etc.)- Wildcard expansion happens at check-time — new permissions added in future releases automatically apply to existing
*andcontent.*roles
# Assign a role to a user
POST /api/v1/users/{userId}/roles
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"role_id": "018e1234-5678-7abc-def0-aaaaaaaaaaaa",
"space_id": null # Optional: null = global assignment
}Response:
{
"user_id": "...",
"role_id": "...",
"space_id": null,
"created_at": "2026-03-07T12:00:00Z"
}- Go to Settings → Users
- Click on a user
- In the "Roles" section, click + Add Role
- Select a role and optional space
- Click Assign
A user's effective permissions are the union of all roles assigned to them (in the active space + global):
User's roles in Space A: [Editor, CustomRole]
User's global roles: [Viewer]
─────────────────────────────────────
Effective permissions: Editor ∪ CustomRole ∪ Viewer
If multiple roles have the same AI limits, the most permissive applies:
daily_generations: max across all rolesallowed_models: union across all rolesdaily_cost_limit_usd: max across all roles
Create a token with a subset of your user's permissions:
POST /api/v1/api-tokens
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"name": "CI/CD Bot",
"abilities": ["content.read", "content.create", "pipeline.run"] # Subset only
}The token can only use the abilities you specify — it can never exceed your own permissions.
When you include a token in a request:
Authorization: Bearer token_abc123
GET /api/v1/content/createThe authorization system checks:
- Your user role permissions — do you have
content.createvia a role? - Token ability scope — does the token include
content.createin its abilities?
Both must be true. This is an intersection check:
User permissions: [content.read, content.create, content.delete, pipeline.run]
Token abilities: [content.read, content.create] ← subset
────────────────────────────────────────────────────────
Allowed to use: [content.read, content.create]
Tokens support wildcard abilities:
{
"abilities": ["content.*", "pipeline.run"]
}The same wildcard expansion rules apply:
*grants everythingcontent.*grants allcontent.*permissionspipeline.*grants allpipeline.*permissions
POST /api/v1/roles
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"space_id": "018e1234-5678-7abc-def0-aaaaaaaaaaaa",
"name": "Content Reviewer",
"slug": "content-reviewer",
"description": "Reviews and approves AI-generated content",
"permissions": [
"content.read",
"content.update",
"pipeline.approve",
"pipeline.reject"
],
"ai_limits": {
"daily_generations": 0,
"allowed_models": [] # No AI generation for this role
}
}PUT /api/v1/roles/{roleId}
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"permissions": ["content.read", "content.update", "pipeline.approve"],
"ai_limits": { ... }
}DELETE /api/v1/roles/{roleId}
Authorization: Bearer YOUR_TOKENSystem roles (is_system: true) can be edited but not deleted.
Each role can have AI generation limits configured in the ai_limits JSON field:
{
"daily_generations": 50,
"daily_image_generations": 10,
"monthly_cost_limit_usd": 100.00,
"allowed_models": ["claude-haiku-4-5", "claude-sonnet-4-6"],
"max_tokens_per_request": 4096,
"require_approval_above_cost_usd": 0.50
}When a user attempts to generate content:
- Effective limits are computed from all their assigned roles (union of most-permissive values)
- Checks happen before the API call:
- Is the requested model in
allowed_models? - Have we hit
daily_generationstoday? - Would this exceed
monthly_cost_limit_usd?
- Is the requested model in
- If within limits → generation proceeds
- If above cost threshold → requires human approval (pipeline pauses)
A role with ai.budget.unlimited permission bypasses all numeric limits:
{
"permissions": ["ai.generate", "ai.budget.unlimited"]
}GET /api/v1/audit-logs
Authorization: Bearer YOUR_TOKENQuery parameters:
| Param | Type | Description |
|---|---|---|
user_id |
string | Filter by user |
action |
string | Filter by action (e.g., content.publish, role.assign) |
resource_type |
string | Filter by resource model (e.g., App\Models\Content) |
from |
ISO-8601 | Earliest timestamp |
to |
ISO-8601 | Latest timestamp |
per_page |
integer | Results per page (default: 50) |
page |
integer | Page number |
| Action | Logged When |
|---|---|
content.publish |
Content is published |
content.delete |
Content is permanently deleted |
pipeline.run |
Pipeline execution starts |
pipeline.approve |
Human approves a run |
role.assign |
Role is assigned to a user |
role.revoke |
Role is revoked from a user |
ai.generation |
AI text generation completes |
ai.generation.failed |
AI generation fails |
ai.budget.exceeded |
User exceeds budget limit |
users.create |
New user is invited |
users.delete |
User account is deleted |
permission.denied |
Permission check fails (attempted unauthorized action) |
{
"id": "018e1234-5678-7abc-def0-cccccccccccc",
"user_id": "018e1234-5678-7abc-def0-aaaaaaaaaaaa",
"space_id": "018e1234-5678-7abc-def0-bbbbbbbbbbbb",
"action": "content.publish",
"resource_type": "App\\Models\\Content",
"resource_id": "018e1234-5678-7abc-def0-dddddddddddd",
"metadata": {
"version": 3,
"previous_status": "draft",
"scheduled_for": "2026-03-10T09:00:00Z"
},
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"created_at": "2026-03-07T12:34:56Z"
}Audit logs are append-only (no updates or deletes). A configurable retention policy (default: 90 days) is enforced by:
php artisan numen:audit:prune --days=90In production, your database user should not have DELETE privileges on the audit_logs table.
use App\Services\AuthorizationService;
class ContentController extends Controller
{
public function store(StoreContentRequest $request, AuthorizationService $authz)
{
$authz->authorize(auth()->user(), 'content.create', $request->space_id);
// Proceed with content creation
}
}Route::post('/content', [ContentController::class, 'store'])
->middleware('permission:content.create');
// Multiple permissions (AND logic)
Route::delete('/content/{id}', [ContentController::class, 'destroy'])
->middleware('permission:content.delete');if ($user->can('content.publish')) {
// User has permission
}
@can('pipeline.approve')
<button>Approve</button>
@endcan$authz = app(AuthorizationService::class);
$perms = $authz->userPermissions(auth()->user(), $spaceId);
// Returns: ['content.read', 'content.create', 'pipeline.run', ...]AuditLog::create([
'user_id' => auth()->user()->id,
'space_id' => $space->id,
'action' => 'content.publish',
'resource_type' => Content::class,
'resource_id' => $content->id,
'metadata' => ['version' => 3],
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'created_at' => now(),
]);Or use the helper:
app(AuthorizationService::class)->log(
user: auth()->user(),
action: 'content.publish',
resource: $content,
metadata: ['version' => 3],
);The RBAC system prevents users from assigning roles with more permissions than they have.
Example:
User A has: [Editor role]
User A tries to assign [Admin role] to User B
→ DENIED: User A cannot escalate beyond their own permissions
This is enforced in AuthorizationService::authorize().
GET /api/v1/roles — List roles in current space
POST /api/v1/roles — Create a custom role
PUT /api/v1/roles/{roleId} — Update role permissions/limits
DELETE /api/v1/roles/{roleId} — Delete role (system roles excepted)
GET /api/v1/roles/{roleId}/users — List users with this role
GET /api/v1/users/{userId}/roles — List roles assigned to user
POST /api/v1/users/{userId}/roles — Assign role to user
DELETE /api/v1/users/{userId}/roles/{roleId} — Revoke role
GET /api/v1/audit-logs — Query audit log (filterable by user, action, resource, date)
GET /api/v1/permissions — List all valid permission strings (permission taxonomy)
- Principle of Least Privilege — grant only the permissions users need
- Regular Audits — review
audit_logsweekly for suspicious patterns - Token Expiration — rotate personal access tokens regularly
- Monitor Budget Limits — set realistic
ai_limitsto catch runaway usage - Backup Audit Logs — the system prunes after 90 days; export important logs to cold storage
- API Token Rotation — cycle tokens when users leave the team
- Read-Only Audit Access — give audit.view sparingly; use for compliance reviews
Check that:
- Your user has the required role assigned
- The role includes the required permission
- If using a token, the token's abilities include the permission
- The role is assigned in the correct space (if space-scoped)
You're trying to assign a role with more permissions than you have. Have an Admin do it instead.
A user has hit their daily_generations or daily_cost_limit_usd. Wait until tomorrow or ask an Admin to increase the limit or grant ai.budget.unlimited.
Check that the action is in the list of logged actions (see What Gets Logged above). Not all actions are audited — only sensitive ones.
# 1. Create the user (via admin panel or API)
# 2. Get the Author role ID
AUTHOR_ROLE_ID="018e1234-5678-7abc-def0-aaaaaaaaaaaa"
# 3. Assign the role
curl -X POST https://yoursite.com/api/v1/users/{userId}/roles \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"role_id": "'$AUTHOR_ROLE_ID'"}'# 1. Create a bot user (via admin panel)
# 2. Create a token for it
curl -X POST https://yoursite.com/api/v1/api-tokens \
-H "Authorization: Bearer BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub Actions Bot",
"abilities": ["content.read", "content.create", "pipeline.run"]
}'
# 3. Use the token in your CI/CD workflow
export NUMEN_TOKEN="token_..."
curl -X POST https://yoursite.com/api/v1/briefs \
-H "Authorization: Bearer $NUMEN_TOKEN" \
-d '{ ... }'curl "https://yoursite.com/api/v1/audit-logs?user_id={userId}&from=2026-03-01T00:00:00Z&to=2026-03-07T23:59:59Z" \
-H "Authorization: Bearer YOUR_TOKEN"- Architecture & Design — docs/architecture/permissions-architecture.md
- Security Review — docs/security-review.md
- API Reference — OpenAPI Spec