Files
poimen-memory/docs/RBAC.md
T
rock ff3e48504c 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
2026-09-01 09:44:52 -07:00

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-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:
    {
      "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:
    {
      "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:
    {
      "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:
      {"error": "forbidden", "reason": "missing capability: memory:write"}
      

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:

  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