# 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: ```yaml # 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: ```yaml name: admin rules: - resources: ["*"] verbs: [read, write, delete, query] ``` ### portfolio-agent Public visitor via portfolio site: ```yaml 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: ```yaml 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: ```json { "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:** ```python # 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:** ```python # 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-roles` to **Scope Mappings** - Add `memory-permissions` to **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 1. **Visitor** opens portfolio site 2. **Portfolio site** authenticates with Authentik using service account 3. **Authentik** returns JWT: ```json { "sub": "portfolio-agent-sa", "permissions": ["memory:read"], "roles": ["portfolio-agent"] } ``` 4. **Portfolio site** calls Memory API: ``` GET /memory/query?project=homelab&query=kubernetes Authorization: Bearer ``` 5. **Memory API**: - Validates JWT ✓ - Checks `memory:read` permission ✓ - Resolves `portfolio-agent` role - Searches homelab project - Filters results: only `visibility: public` - Returns filtered results ### Scenario: Authenticated User Accesses Private Docs 1. **User** logs into app via Authentik 2. **Authentik** returns JWT: ```json { "sub": "alice", "permissions": ["memory:read", "memory:write"], "groups": ["engineering"], "roles": ["authenticated-user"] } ``` 3. **User** queries private docs: ``` GET /memory/query?project=homelab&query=internal%20secrets ``` 4. **Memory API**: - Validates JWT ✓ - Checks `memory:read` ✓ - Resolves `authenticated-user` role - No visibility restriction → includes private docs ✓ - Returns all matching results ### Scenario: User Tries to Write Without Permission 1. **User** has read-only JWT: ```json { "sub": "viewer", "permissions": ["memory:read"], "roles": ["portfolio-agent"] } ``` 2. **User** tries to ingest: ``` POST /memory/ingest ``` 3. **Memory API**: - Checks `memory:write` permission ✗ - Returns `403 Forbidden`: ```json {"error": "forbidden", "reason": "missing capability: memory:write"} ``` --- ## Custom Roles ### Creating a Team Role ```yaml # 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: ```bash RBAC_ROLES_DIR=/app/config/roles ``` Or use Kubernetes ConfigMap: ```yaml 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 ```bash # 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 ```bash # List accessible projects curl -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/memory/projects # Should only return projects your role can access ``` ### Test Visibility Filtering ```bash # 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:** 1. Check user's roles in JWT 2. Verify role's `scope.projects` includes the project 3. Or add user to a group with project access ### Results Missing (RBAC Filtered) Private docs filtered out due to visibility scope. **Check:** 1. Document visibility (public/private) 2. User's role visibility scope 3. Enable debug logging: `RUST_LOG=debug` ### No Roles in JWT Authentik not configured to include roles. **Fix:** 1. Create scope mapping for roles 2. Add mapping to OAuth2 provider 3. Request `roles` scope 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` |