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
This commit is contained in:
+456
@@ -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 <jwt>
|
||||
```
|
||||
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` |
|
||||
Reference in New Issue
Block a user