From ff3e48504cb29ad285ebdd8ffda6a09fcf5cbb56 Mon Sep 17 00:00:00 2001 From: rock Date: Tue, 1 Sep 2026 09:44:52 -0700 Subject: [PATCH] docs: API.md + RBAC.md with Authentik integration 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 --- crates/mem-cli/src/http_server.rs | 41 ++- crates/mem-cli/src/jwt_validator.rs | 2 + docs/API.md | 414 +++++++++++++++++++++++++ docs/RBAC.md | 456 ++++++++++++++++++++++++++++ 4 files changed, 907 insertions(+), 6 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/RBAC.md diff --git a/crates/mem-cli/src/http_server.rs b/crates/mem-cli/src/http_server.rs index 52593a4..63e5bac 100644 --- a/crates/mem-cli/src/http_server.rs +++ b/crates/mem-cli/src/http_server.rs @@ -123,6 +123,7 @@ fn validate_apikey(req: &HttpRequest, state: &AppState) -> Result<(JwtClaims, St nbf: None, permissions: Some(vec!["*".to_string()]), groups: None, + roles: Some(vec!["admin".to_string()]), // API key gets admin role }; Ok((claims, "apikey".to_string())) @@ -150,6 +151,7 @@ fn extract_rate_limit_key(claims: &JwtClaims) -> String { /// Convert JWT claims to RBAC claims for AccessGuard fn to_rbac_claims(jwt: &JwtClaims) -> RbacClaims { RbacClaims::new(&jwt.sub) + .with_roles(jwt.roles.clone().unwrap_or_default().iter().map(|s| s.as_str()).collect()) .with_groups(jwt.groups.clone().unwrap_or_default().iter().map(|s| s.as_str()).collect()) .with_permissions(jwt.permissions.clone().unwrap_or_default().iter().map(|s| s.as_str()).collect()) } @@ -1519,6 +1521,28 @@ pub async fn vault_file_handler( mod tests { use super::*; + #[test] + fn test_to_rbac_claims_with_roles() { + let jwt = JwtClaims { + sub: "alice".to_string(), + iss: "authentik".to_string(), + aud: "memory".to_string(), + exp: i64::MAX, + iat: 0, + nbf: None, + permissions: Some(vec!["memory:read".to_string()]), + groups: Some(vec!["engineering".to_string()]), + roles: Some(vec!["authenticated-user".to_string(), "homelab-team".to_string()]), + }; + + let rbac = to_rbac_claims(&jwt); + + assert_eq!(rbac.sub, "alice"); + assert!(rbac.has_role("authenticated-user")); + assert!(rbac.has_role("homelab-team")); + assert!(!rbac.has_role("admin")); + } + #[test] fn test_to_rbac_claims_basic() { let jwt = JwtClaims { @@ -1530,6 +1554,7 @@ mod tests { nbf: None, permissions: Some(vec!["memory:read".to_string(), "memory:write".to_string()]), groups: Some(vec!["engineering".to_string(), "ml-team".to_string()]), + roles: Some(vec!["authenticated-user".to_string()]), }; let rbac = to_rbac_claims(&jwt); @@ -1552,6 +1577,7 @@ mod tests { nbf: None, permissions: None, groups: None, + roles: None, }; let rbac = to_rbac_claims(&jwt); @@ -1631,7 +1657,7 @@ mod tests { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); - // Admin JWT + // Admin JWT with roles from Authentik let jwt = JwtClaims { sub: "admin-user".to_string(), iss: "test".to_string(), @@ -1641,8 +1667,9 @@ mod tests { nbf: None, permissions: Some(vec!["*".to_string()]), groups: None, + roles: Some(vec!["admin".to_string()]), }; - let rbac_claims = to_rbac_claims(&jwt).with_roles(vec!["admin"]); + let rbac_claims = to_rbac_claims(&jwt); // Admin can access any project let project = ResourceMeta::new("secret-project", ResourceType::Project, "secret-project"); @@ -1657,7 +1684,7 @@ mod tests { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); - // Portfolio agent JWT + // Portfolio agent JWT with roles from Authentik let jwt = JwtClaims { sub: "visitor-123".to_string(), iss: "test".to_string(), @@ -1667,8 +1694,9 @@ mod tests { nbf: None, permissions: Some(vec!["memory:read".to_string()]), groups: None, + roles: Some(vec!["portfolio-agent".to_string()]), }; - let rbac_claims = to_rbac_claims(&jwt).with_roles(vec!["portfolio-agent"]); + let rbac_claims = to_rbac_claims(&jwt); // Can read public wiki in allowed project let public_wiki = ResourceMeta::wiki("doc-1", "homelab") @@ -1692,7 +1720,7 @@ mod tests { let guard = AccessGuard::new(Arc::new(builtin_role_provider())); - // JWT with no roles + // JWT with no roles (anonymous user) let jwt = JwtClaims { sub: "anonymous".to_string(), iss: "test".to_string(), @@ -1702,8 +1730,9 @@ mod tests { nbf: None, permissions: None, groups: None, + roles: None, // No roles assigned }; - let rbac_claims = to_rbac_claims(&jwt); // No roles + let rbac_claims = to_rbac_claims(&jwt); // Cannot read anything without a role let wiki = ResourceMeta::wiki("doc", "homelab") diff --git a/crates/mem-cli/src/jwt_validator.rs b/crates/mem-cli/src/jwt_validator.rs index 22e5d70..40c716f 100644 --- a/crates/mem-cli/src/jwt_validator.rs +++ b/crates/mem-cli/src/jwt_validator.rs @@ -17,6 +17,8 @@ pub struct JwtClaims { pub nbf: Option, pub permissions: Option>, pub groups: Option>, + /// Roles from Authentik (for RBAC) + pub roles: Option>, } /// JWKS (JSON Web Key Set) response from Authentik diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..420f641 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,414 @@ +# Memory API Reference + +## Authentication + +All endpoints require authentication via JWT token (from Authentik) or API key fallback. + +### JWT Authentication (Recommended) + +```bash +# Get token from Authentik +TOKEN=$(curl -s -X POST https://authentik.riotpiao.com/application/o/token/ \ + -d "grant_type=client_credentials" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" | jq -r '.access_token') + +# Use token in requests +curl -H "Authorization: Bearer $TOKEN" \ + http://memory.riotpiao.com/memory/query?project=homelab&query=kubernetes +``` + +### API Key Authentication (Fallback) + +```bash +curl -H "apikey: YOUR_API_KEY" \ + http://memory.riotpiao.com/memory/query?project=homelab&query=kubernetes +``` + +--- + +## Endpoints + +### Health Check + +```http +GET /health +``` + +**Response:** +```json +{ + "status": "ok", + "uptime_secs": 3600 +} +``` + +--- + +### Query Memory + +Search learned knowledge using semantic + hybrid search. + +```http +GET /memory/query?project={project}&query={query}&limit={limit}&method={method} +``` + +**Parameters:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| project | string | Yes | Project to search in | +| query | string | Yes | Search query | +| limit | int | No | Max results (default: 10) | +| method | string | No | `semantic` or `hybrid` (default: hybrid) | + +**Example:** +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "http://memory.riotpiao.com/memory/query?project=homelab&query=fix%20kubernetes%20port%20conflict&limit=5" +``` + +**Response:** +```json +{ + "query": "fix kubernetes port conflict", + "project": "homelab", + "method": "hybrid", + "results": [ + { + "level": "L1", + "score": 0.92, + "text": "To fix port conflicts in Kubernetes...", + "source": "troubleshooting/ports.md", + "provenance": ["session-123"] + } + ] +} +``` + +**RBAC:** Requires `memory:read` permission. Results filtered by user's project/visibility access. + +--- + +### Context Lookup (Three-Tier RAG) + +Get contextual knowledge for tool/task with failure diagnosis. + +```http +POST /memory/context +Content-Type: application/json +``` + +**Body:** +```json +{ + "project": "homelab", + "tool": "kubectl", + "task": "debug-pod", + "scope": "tool_context", + "budget": 8192, + "failure_log": "CrashLoopBackOff: container exited with code 1" +} +``` + +**Example:** +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"project":"homelab","tool":"kubectl","task":"debug-pod","budget":4096}' \ + http://memory.riotpiao.com/memory/context +``` + +**Response:** +```json +{ + "tier": 1, + "lessons": [ + { + "tier": 1, + "level": "L1", + "score": 1.0, + "text": "CrashLoopBackOff usually means...", + "matched_kind": "symptom", + "seen_count": 15 + } + ], + "skills": [ + { + "name": "diagnose-pod-failure", + "score": 0.95, + "description": "Debug Kubernetes pod crashes" + } + ], + "budget": { + "limit": 4096, + "used": 2048, + "dropped": [] + } +} +``` + +**RBAC:** Requires `memory:read` permission + project access. + +--- + +### Ingest Records + +Add new knowledge to memory. + +```http +POST /memory/ingest +Content-Type: application/json +``` + +**Body:** +```json +{ + "project": "homelab", + "ingest_id": "session-2024-01-15-001", + "source": "conversation://claude/session-123", + "records": [ + {"text": "Kubernetes uses port 6443 for API server..."}, + {"text": "To change the port, edit /etc/kubernetes/manifests/kube-apiserver.yaml"} + ] +} +``` + +**Example:** +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"project":"homelab","ingest_id":"test-001","source":"manual","records":[{"text":"Test fact"}]}' \ + http://memory.riotpiao.com/memory/ingest +``` + +**Response:** +```json +{ + "status": "accepted", + "ingest_id": "test-001", + "records_queued": 1 +} +``` + +**RBAC:** Requires `memory:write` permission + project write access. + +--- + +### Learn from Text + +Process and learn from a block of text (chunking + embedding + synthesis). + +```http +POST /memory/learn +Content-Type: application/json +``` + +**Body:** +```json +{ + "project": "homelab", + "text": "# Kubernetes Networking\n\nKubernetes uses CNI plugins...", + "query": "What are the key networking concepts?", + "chunk_size": 2000, + "memory_budget": 4096 +} +``` + +**Example:** +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"project":"homelab","text":"# Guide\nSome content...","query":"Summarize this"}' \ + http://memory.riotpiao.com/memory/learn +``` + +**RBAC:** Requires `memory:write` permission + project write access. + +--- + +### List Projects + +Get all accessible projects. + +```http +GET /memory/projects +``` + +**Example:** +```bash +curl -H "Authorization: Bearer $TOKEN" \ + http://memory.riotpiao.com/memory/projects +``` + +**Response:** +```json +{ + "projects": ["homelab", "portfolio"], + "count": 2 +} +``` + +**RBAC:** Returns only projects user has read access to. + +--- + +### List Skills + +Get extracted skills. + +```http +GET /memory/skills +``` + +**Response:** +```json +{ + "skills": [ + { + "name": "diagnose-pod-failure", + "description": "Debug Kubernetes pod issues", + "when_to_use": "Pod in CrashLoopBackOff or Error state" + } + ], + "count": 1 +} +``` + +**RBAC:** Requires `memory:read` permission. + +--- + +### Ingest Status + +Check status of an ingest job. + +```http +GET /memory/ingest/{ingest_id} +``` + +**Example:** +```bash +curl -H "Authorization: Bearer $TOKEN" \ + http://memory.riotpiao.com/memory/ingest/session-2024-01-15-001 +``` + +**Response:** +```json +{ + "ingest_id": "session-2024-01-15-001", + "status": "completed", + "records_processed": 5, + "created_at": "2024-01-15T10:30:00Z", + "completed_at": "2024-01-15T10:30:05Z" +} +``` + +--- + +## Error Responses + +### 401 Unauthorized +```json +{ + "error": "unauthorized", + "reason": "missing Authorization header" +} +``` + +### 403 Forbidden +```json +{ + "error": "forbidden", + "reason": "missing capability: memory:write" +} +``` + +Or with RBAC: +```json +{ + "error": "forbidden", + "reason": "access denied to project 'secret-project'" +} +``` + +### 429 Too Many Requests +```json +{ + "error": "rate_limited", + "reason": "exceeded 1000 requests/hour for /memory/query" +} +``` + +--- + +## Rate Limits + +| Endpoint | Limit | +|----------|-------| +| `/memory/ingest` | 100/hour | +| `/memory/query` | 1000/hour | +| `/memory/context` | 100/hour | +| `/memory/learn` | 100/hour | + +Rate limits are per-user (based on JWT `sub` claim). + +--- + +## SDK Examples + +### Python + +```python +import requests + +class MemoryClient: + def __init__(self, base_url, token): + self.base_url = base_url + self.headers = {"Authorization": f"Bearer {token}"} + + def query(self, project, query, limit=10): + resp = requests.get( + f"{self.base_url}/memory/query", + params={"project": project, "query": query, "limit": limit}, + headers=self.headers + ) + resp.raise_for_status() + return resp.json() + + def ingest(self, project, records, source="api"): + import uuid + resp = requests.post( + f"{self.base_url}/memory/ingest", + json={ + "project": project, + "ingest_id": str(uuid.uuid4()), + "source": source, + "records": [{"text": r} for r in records] + }, + headers=self.headers + ) + resp.raise_for_status() + return resp.json() + +# Usage +client = MemoryClient("http://memory.riotpiao.com", TOKEN) +results = client.query("homelab", "kubernetes networking") +``` + +### curl One-Liners + +```bash +# Query +curl -H "Authorization: Bearer $TOKEN" \ + "http://memory.riotpiao.com/memory/query?project=homelab&query=kubernetes" + +# Ingest +curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{"project":"homelab","ingest_id":"'$(uuidgen)'","source":"cli","records":[{"text":"New fact"}]}' \ + http://memory.riotpiao.com/memory/ingest + +# Context +curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{"project":"homelab","tool":"kubectl","task":"debug"}' \ + http://memory.riotpiao.com/memory/context +``` diff --git a/docs/RBAC.md b/docs/RBAC.md new file mode 100644 index 0000000..12a9fcb --- /dev/null +++ b/docs/RBAC.md @@ -0,0 +1,456 @@ +# 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` |