Documentation: - docs/API.md: Complete API reference with examples - All endpoints with curl examples - Python SDK example - Error responses and rate limits - docs/RBAC.md: RBAC system documentation - Two-level access control explained - Built-in roles (admin, portfolio-agent, authenticated-user) - Authentik configuration guide - Scope mapping examples for roles/permissions - Troubleshooting guide JWT Integration: - Add 'roles' field to JwtClaims struct - Wire roles from Authentik JWT to RBAC Claims - API key users get 'admin' role by default Tests: - Add test_to_rbac_claims_with_roles - Verify roles extraction from JWT - 670 tests passing
11 KiB
RBAC (Role-Based Access Control)
Overview
The Memory system uses a hierarchical RBAC model integrated with Authentik OIDC:
┌─────────────────────────────────────────────────────────────┐
│ Authentik (OIDC) │
│ Issues JWT with: sub, roles, groups, permissions │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Memory API Server │
│ │
│ 1. Validate JWT │
│ 2. Check capability (memory:read / memory:write) │
│ 3. Resolve roles → load AccessRules │
│ 4. Evaluate scopes (project, visibility, owner) │
│ 5. Filter results by access │
└─────────────────────────────────────────────────────────────┘
Two-Level Access Control
Level 1: Capabilities (HTTP Layer)
Broad permissions checked at endpoint level:
| Capability | Endpoints |
|---|---|
memory:read |
/memory/query, /memory/context, /memory/projects, /memory/skills |
memory:write |
/memory/ingest, /memory/learn |
* |
All (wildcard) |
These come from JWT permissions claim.
Level 2: Resource Access (RBAC Layer)
Fine-grained access based on roles and scopes:
- Project scope: Which projects can user access?
- Visibility scope: Public only, or also private?
- Owner scope: Own resources only, or all?
- Group scope: Required group membership?
Core Concepts
Roles
A role is a named set of access rules:
# config/roles/portfolio-agent.yaml
name: portfolio-agent
description: Public visitor access via portfolio site
rules:
- resources: [wiki, embedding]
verbs: [read, query]
scope:
projects: [homelab, rbc, aws, portfolio]
visibility: public
- resources: [conversation]
verbs: [read, write]
scope:
projects: [portfolio]
owner: self
Access Rules
Each rule specifies:
| Field | Description | Example |
|---|---|---|
resources |
Resource types | [wiki, embedding, conversation, skill, project] or ["*"] |
verbs |
Allowed actions | [read, write, delete, query] |
scope |
Constraints | See below |
Scopes
| Scope | Description | Values |
|---|---|---|
projects |
Allowed project names | ["homelab", "portfolio"] or ["*"] |
visibility |
Document visibility | public or private (omit for both) |
owner |
Owner constraint | self (own only), any, or specific user ID |
groups |
Required groups | ["engineering", "ml-team"] |
Built-in Roles
admin
Full access to everything:
name: admin
rules:
- resources: ["*"]
verbs: [read, write, delete, query]
portfolio-agent
Public visitor via portfolio site:
name: portfolio-agent
rules:
# Read public wiki/embeddings from allowed projects
- resources: [wiki, embedding]
verbs: [read, query]
scope:
projects: [homelab, rbc, aws, portfolio]
visibility: public
# Manage own conversations in portfolio only
- resources: [conversation]
verbs: [read, write]
scope:
projects: [portfolio]
owner: self
authenticated-user
Logged-in user via Authentik:
name: authenticated-user
rules:
# Read all wiki/skills (including private)
- resources: [wiki, embedding, skill]
verbs: [read, query]
# Manage own conversations anywhere
- resources: [conversation]
verbs: [read, write, delete]
scope:
owner: self
Authentik Integration
JWT Claims
The Memory API expects these claims in JWT:
{
"sub": "alice",
"iss": "https://authentik.riotpiao.com/application/o/poimen-memory/",
"aud": "poimen-memory",
"exp": 1735689600,
"permissions": ["memory:read", "memory:write"],
"groups": ["engineering", "ml-team"],
"roles": ["authenticated-user", "homelab-team"]
}
Authentik Configuration
1. Create Application
Name: poimen-memory
Slug: poimen-memory
Provider: OAuth2/OIDC
2. Create OAuth2 Provider
Name: poimen-memory-provider
Client type: Confidential
Redirect URIs: https://memory.riotpiao.com/callback
Scopes: openid profile email
3. Add Custom Scopes for Roles
Create a Scope Mapping to include roles in JWT:
Name: memory-roles
Scope name: roles
Expression:
# Return user's groups that match memory roles
role_groups = ["admin", "portfolio-agent", "authenticated-user", "homelab-team"]
return {
"roles": [g.name for g in user.ak_groups.all() if g.name in role_groups]
}
4. Add Permissions Scope
Name: memory-permissions
Scope name: permissions
Expression:
# Base permissions for all authenticated users
permissions = ["memory:read"]
# Add write permission for specific groups
if user.ak_groups.filter(name__in=["admin", "homelab-team", "writers"]).exists():
permissions.append("memory:write")
# Admin gets wildcard
if user.ak_groups.filter(name="admin").exists():
permissions = ["*"]
return {"permissions": permissions}
5. Assign Scope Mappings to Provider
In the OAuth2 Provider settings:
- Add
memory-rolesto Scope Mappings - Add
memory-permissionsto Scope Mappings
6. Create Groups in Authentik
| Group | Description |
|---|---|
admin |
Full access |
authenticated-user |
Default for logged-in users |
portfolio-agent |
Service account for portfolio site |
homelab-team |
Team members for homelab project |
Access Flow Example
Scenario: Portfolio Visitor Queries Memory
- Visitor opens portfolio site
- Portfolio site authenticates with Authentik using service account
- Authentik returns JWT:
{ "sub": "portfolio-agent-sa", "permissions": ["memory:read"], "roles": ["portfolio-agent"] } - Portfolio site calls Memory API:
GET /memory/query?project=homelab&query=kubernetes Authorization: Bearer <jwt> - Memory API:
- Validates JWT ✓
- Checks
memory:readpermission ✓ - Resolves
portfolio-agentrole - Searches homelab project
- Filters results: only
visibility: public - Returns filtered results
Scenario: Authenticated User Accesses Private Docs
- User logs into app via Authentik
- Authentik returns JWT:
{ "sub": "alice", "permissions": ["memory:read", "memory:write"], "groups": ["engineering"], "roles": ["authenticated-user"] } - User queries private docs:
GET /memory/query?project=homelab&query=internal%20secrets - Memory API:
- Validates JWT ✓
- Checks
memory:read✓ - Resolves
authenticated-userrole - No visibility restriction → includes private docs ✓
- Returns all matching results
Scenario: User Tries to Write Without Permission
- User has read-only JWT:
{ "sub": "viewer", "permissions": ["memory:read"], "roles": ["portfolio-agent"] } - User tries to ingest:
POST /memory/ingest - Memory API:
- Checks
memory:writepermission ✗ - Returns
403 Forbidden:{"error": "forbidden", "reason": "missing capability: memory:write"}
- Checks
Custom Roles
Creating a Team Role
# config/roles/ml-team.yaml
name: ml-team
description: ML team with access to ML projects
rules:
# Full access to ML projects
- resources: [wiki, embedding, skill]
verbs: [read, write, query]
scope:
projects: [ml-experiments, model-training, datasets]
# Read-only access to shared projects
- resources: [wiki, embedding]
verbs: [read, query]
scope:
projects: [homelab, documentation]
visibility: public
# Own conversations only
- resources: [conversation]
verbs: [read, write, delete]
scope:
owner: self
Loading Custom Roles
Set environment variable:
RBAC_ROLES_DIR=/app/config/roles
Or use Kubernetes ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: memory-roles
data:
ml-team.yaml: |
name: ml-team
rules:
- resources: [wiki]
verbs: [read, write]
scope:
projects: [ml-experiments]
Testing RBAC
Check Your Access
# Decode your JWT
echo $TOKEN | cut -d. -f2 | base64 -d | jq
# Test query (should work with memory:read)
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/memory/query?project=homelab&query=test"
# Test ingest (requires memory:write)
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"project":"homelab","ingest_id":"test","source":"test","records":[{"text":"test"}]}' \
http://localhost:8080/memory/ingest
Verify Project Filtering
# List accessible projects
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/memory/projects
# Should only return projects your role can access
Test Visibility Filtering
# As portfolio-agent: should only return public docs
curl -H "Authorization: Bearer $PORTFOLIO_TOKEN" \
"http://localhost:8080/memory/query?project=homelab&query=private"
# As authenticated-user: should return public + private
curl -H "Authorization: Bearer $USER_TOKEN" \
"http://localhost:8080/memory/query?project=homelab&query=private"
Troubleshooting
"missing capability: memory:read"
JWT doesn't have memory:read in permissions claim.
Fix: Update Authentik scope mapping to include memory:read.
"access denied to project 'X'"
User's role doesn't allow access to that project.
Fix:
- Check user's roles in JWT
- Verify role's
scope.projectsincludes the project - Or add user to a group with project access
Results Missing (RBAC Filtered)
Private docs filtered out due to visibility scope.
Check:
- Document visibility (public/private)
- User's role visibility scope
- Enable debug logging:
RUST_LOG=debug
No Roles in JWT
Authentik not configured to include roles.
Fix:
- Create scope mapping for roles
- Add mapping to OAuth2 provider
- Request
rolesscope in token request
Environment Variables
| Variable | Description | Default |
|---|---|---|
RBAC_ROLES_DIR |
Directory for YAML role files | (builtin roles only) |
MEM_AUTH_MODE |
jwt or apikey |
jwt |
AUTHENTIK_ISSUER |
Authentik OIDC issuer URL | required for JWT |
AUTHENTIK_AUDIENCE |
Expected JWT audience | poimen-memory |
JWT_CACHE_TTL_SECS |
JWT validation cache TTL | 3600 |