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:
@@ -123,6 +123,7 @@ fn validate_apikey(req: &HttpRequest, state: &AppState) -> Result<(JwtClaims, St
|
|||||||
nbf: None,
|
nbf: None,
|
||||||
permissions: Some(vec!["*".to_string()]),
|
permissions: Some(vec!["*".to_string()]),
|
||||||
groups: None,
|
groups: None,
|
||||||
|
roles: Some(vec!["admin".to_string()]), // API key gets admin role
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((claims, "apikey".to_string()))
|
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
|
/// Convert JWT claims to RBAC claims for AccessGuard
|
||||||
fn to_rbac_claims(jwt: &JwtClaims) -> RbacClaims {
|
fn to_rbac_claims(jwt: &JwtClaims) -> RbacClaims {
|
||||||
RbacClaims::new(&jwt.sub)
|
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_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())
|
.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 {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_to_rbac_claims_basic() {
|
fn test_to_rbac_claims_basic() {
|
||||||
let jwt = JwtClaims {
|
let jwt = JwtClaims {
|
||||||
@@ -1530,6 +1554,7 @@ mod tests {
|
|||||||
nbf: None,
|
nbf: None,
|
||||||
permissions: Some(vec!["memory:read".to_string(), "memory:write".to_string()]),
|
permissions: Some(vec!["memory:read".to_string(), "memory:write".to_string()]),
|
||||||
groups: Some(vec!["engineering".to_string(), "ml-team".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);
|
let rbac = to_rbac_claims(&jwt);
|
||||||
@@ -1552,6 +1577,7 @@ mod tests {
|
|||||||
nbf: None,
|
nbf: None,
|
||||||
permissions: None,
|
permissions: None,
|
||||||
groups: None,
|
groups: None,
|
||||||
|
roles: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let rbac = to_rbac_claims(&jwt);
|
let rbac = to_rbac_claims(&jwt);
|
||||||
@@ -1631,7 +1657,7 @@ mod tests {
|
|||||||
|
|
||||||
let guard = AccessGuard::new(Arc::new(builtin_role_provider()));
|
let guard = AccessGuard::new(Arc::new(builtin_role_provider()));
|
||||||
|
|
||||||
// Admin JWT
|
// Admin JWT with roles from Authentik
|
||||||
let jwt = JwtClaims {
|
let jwt = JwtClaims {
|
||||||
sub: "admin-user".to_string(),
|
sub: "admin-user".to_string(),
|
||||||
iss: "test".to_string(),
|
iss: "test".to_string(),
|
||||||
@@ -1641,8 +1667,9 @@ mod tests {
|
|||||||
nbf: None,
|
nbf: None,
|
||||||
permissions: Some(vec!["*".to_string()]),
|
permissions: Some(vec!["*".to_string()]),
|
||||||
groups: None,
|
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
|
// Admin can access any project
|
||||||
let project = ResourceMeta::new("secret-project", ResourceType::Project, "secret-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()));
|
let guard = AccessGuard::new(Arc::new(builtin_role_provider()));
|
||||||
|
|
||||||
// Portfolio agent JWT
|
// Portfolio agent JWT with roles from Authentik
|
||||||
let jwt = JwtClaims {
|
let jwt = JwtClaims {
|
||||||
sub: "visitor-123".to_string(),
|
sub: "visitor-123".to_string(),
|
||||||
iss: "test".to_string(),
|
iss: "test".to_string(),
|
||||||
@@ -1667,8 +1694,9 @@ mod tests {
|
|||||||
nbf: None,
|
nbf: None,
|
||||||
permissions: Some(vec!["memory:read".to_string()]),
|
permissions: Some(vec!["memory:read".to_string()]),
|
||||||
groups: None,
|
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
|
// Can read public wiki in allowed project
|
||||||
let public_wiki = ResourceMeta::wiki("doc-1", "homelab")
|
let public_wiki = ResourceMeta::wiki("doc-1", "homelab")
|
||||||
@@ -1692,7 +1720,7 @@ mod tests {
|
|||||||
|
|
||||||
let guard = AccessGuard::new(Arc::new(builtin_role_provider()));
|
let guard = AccessGuard::new(Arc::new(builtin_role_provider()));
|
||||||
|
|
||||||
// JWT with no roles
|
// JWT with no roles (anonymous user)
|
||||||
let jwt = JwtClaims {
|
let jwt = JwtClaims {
|
||||||
sub: "anonymous".to_string(),
|
sub: "anonymous".to_string(),
|
||||||
iss: "test".to_string(),
|
iss: "test".to_string(),
|
||||||
@@ -1702,8 +1730,9 @@ mod tests {
|
|||||||
nbf: None,
|
nbf: None,
|
||||||
permissions: None,
|
permissions: None,
|
||||||
groups: 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
|
// Cannot read anything without a role
|
||||||
let wiki = ResourceMeta::wiki("doc", "homelab")
|
let wiki = ResourceMeta::wiki("doc", "homelab")
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ pub struct JwtClaims {
|
|||||||
pub nbf: Option<i64>,
|
pub nbf: Option<i64>,
|
||||||
pub permissions: Option<Vec<String>>,
|
pub permissions: Option<Vec<String>>,
|
||||||
pub groups: Option<Vec<String>>,
|
pub groups: Option<Vec<String>>,
|
||||||
|
/// Roles from Authentik (for RBAC)
|
||||||
|
pub roles: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JWKS (JSON Web Key Set) response from Authentik
|
/// JWKS (JSON Web Key Set) response from Authentik
|
||||||
|
|||||||
+414
@@ -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
|
||||||
|
```
|
||||||
+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