iam: add memory scope + service accounts to Authentik
- Add 'memory' scope property mapping with claims: - memory_projects: project-level access filter - memory_visibility: public/private visibility - memory_role: admin/user/portfolio-agent - Add capability groups: llm-users, memory-users, memory-writers - Add service account provisioning for portfolio-agent, memory-agent - Service accounts use client_credentials flow - Add homelab-rbac.md design doc (Authentik-centric model)
This commit is contained in:
+409
@@ -0,0 +1,409 @@
|
|||||||
|
# Homelab RBAC Design
|
||||||
|
|
||||||
|
**Authentik as Universal Identity Provider** — Single source of truth for all identity, groups, and permissions. Services validate JWTs and trust claims; no separate auth systems.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Authentik = Policy Decision Point** — All access decisions computed in Authentik, embedded in JWT claims
|
||||||
|
2. **Services = Policy Enforcement Points** — Validate JWT signature via JWKS, trust claims blindly
|
||||||
|
3. **Zero Service-Side RBAC** — No local user/role tables, no permission logic in services
|
||||||
|
4. **Claims Are The API** — Each service defines what claims it needs; Authentik provides them
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Clients │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Browser │ │ CLI │ │ Mobile │ │ Agent │ │ CI/CD │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
└────────┼─────────────┼─────────────┼─────────────┼─────────────┼────────────┘
|
||||||
|
│ │ │ │ │
|
||||||
|
│ auth_code │ device │ device │ client │ client
|
||||||
|
│ + PKCE │ code │ code │ credentials│ credentials
|
||||||
|
│ │ │ │ │
|
||||||
|
└─────────────┴─────────────┴─────────────┴─────────────┴─────────────┐
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ AUTHENTIK (Policy Decision Point) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────────────────────┐ │
|
||||||
|
│ │ Users │ │ Groups │ │ User Attributes │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ rock (human) │ │ homelab-admins │ │ memory_projects: ["*"] │ │
|
||||||
|
│ │ portfolio-sa │ │ llm-admins │ │ memory_visibility: private │ │
|
||||||
|
│ │ (svc account) │ │ memory-admins │ │ minio_policy: consoleAdmin │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └───────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Property Mappings (Scope → Claim) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ scope: groups → claim: groups (group names list) │ │
|
||||||
|
│ │ scope: permissions → claim: permissions (computed from groups) │ │
|
||||||
|
│ │ scope: memory → claims: memory_projects, memory_visibility, │ │
|
||||||
|
│ │ memory_role (from user attrs + groups) │ │
|
||||||
|
│ │ scope: minio → claim: policy (MinIO policy name) │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ JWT Token (signed) │ │
|
||||||
|
│ │ { │ │
|
||||||
|
│ │ "sub": "abc123", │ │
|
||||||
|
│ │ "groups": ["homelab-admins", "memory-admins"], │ │
|
||||||
|
│ │ "permissions": ["*"], │ │
|
||||||
|
│ │ "memory_projects": ["*"], │ │
|
||||||
|
│ │ "memory_visibility": "private", │ │
|
||||||
|
│ │ "memory_role": "admin", │ │
|
||||||
|
│ │ "policy": "consoleAdmin" │ │
|
||||||
|
│ │ } │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────────────────────┬────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
JWT (Authorization: Bearer)
|
||||||
|
│
|
||||||
|
┌──────────────────────────────┼──────────────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
|
||||||
|
│ API Gateway │ │ Memory Service │ │ MinIO │
|
||||||
|
│ │ │ (Poimen) │ │ │
|
||||||
|
│ 1. Validate JWT │ │ 1. Validate JWT │ │ 1. Validate JWT │
|
||||||
|
│ 2. Read claims: │ │ 2. Read claims: │ │ 2. Read claims: │
|
||||||
|
│ - permissions │ │ - memory_* │ │ - policy │
|
||||||
|
│ 3. Allow/deny │ │ 3. Filter rows │ │ 3. Map to IAM │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ NO local RBAC │ │ NO local RBAC │ │ NO local RBAC │
|
||||||
|
└───────────────────┘ └───────────────────┘ └───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentik Configuration
|
||||||
|
|
||||||
|
### Groups (in Authentik)
|
||||||
|
|
||||||
|
| Group | Purpose | `is_superuser` |
|
||||||
|
|-------|---------|----------------|
|
||||||
|
| `homelab-admins` | Universal admin, full access to everything | `true` |
|
||||||
|
| `llm-admins` | LLM service management | `false` |
|
||||||
|
| `memory-admins` | Memory service management | `false` |
|
||||||
|
| `minio-admins` | MinIO storage management | `false` |
|
||||||
|
| `llm-users` | Can call LLM inference (no admin) | `false` |
|
||||||
|
| `memory-users` | Can query memory (no write) | `false` |
|
||||||
|
|
||||||
|
### User Attributes (stored on user object)
|
||||||
|
|
||||||
|
Fine-grained scopes stored as user attributes in Authentik:
|
||||||
|
|
||||||
|
| Attribute | Type | Example | Purpose |
|
||||||
|
|-----------|------|---------|---------|
|
||||||
|
| `memory_projects` | `string[]` | `["homelab", "portfolio"]` or `["*"]` | Which projects user can access |
|
||||||
|
| `memory_visibility` | `string` | `"public"` or `"private"` | Max visibility level |
|
||||||
|
| `minio_policy` | `string` | `"portfolio-rw"` | Override MinIO policy (optional) |
|
||||||
|
|
||||||
|
### Property Mappings (Scopes → Claims)
|
||||||
|
|
||||||
|
#### 1. `permissions` scope (existing)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Already in authentik-provision.py
|
||||||
|
GROUP_PERMISSIONS = {
|
||||||
|
"homelab-admins": ["*"],
|
||||||
|
"llm-admins": ["llm:read", "llm:write", "llm:inference"],
|
||||||
|
"llm-users": ["llm:inference"],
|
||||||
|
"memory-admins": ["memory:read", "memory:write", "memory:admin"],
|
||||||
|
"memory-users": ["memory:read"],
|
||||||
|
"minio-admins": ["minio:read", "minio:write"],
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
perms = set()
|
||||||
|
for group in request.user.groups.all():
|
||||||
|
perms.update(GROUP_PERMISSIONS.get(group.name, []))
|
||||||
|
return {"permissions": sorted(perms)}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. `memory` scope (NEW - to add)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Memory service claims - reads from user attributes + group membership
|
||||||
|
_MEMORY_EXPR = """
|
||||||
|
# Default scopes
|
||||||
|
projects = request.user.attributes.get("memory_projects", [])
|
||||||
|
visibility = request.user.attributes.get("memory_visibility", "public")
|
||||||
|
role = "user"
|
||||||
|
|
||||||
|
# Admin overrides
|
||||||
|
if request.user.ak_groups.filter(name="homelab-admins").exists():
|
||||||
|
projects = ["*"]
|
||||||
|
visibility = "private"
|
||||||
|
role = "admin"
|
||||||
|
elif request.user.ak_groups.filter(name="memory-admins").exists():
|
||||||
|
visibility = "private"
|
||||||
|
role = "admin"
|
||||||
|
|
||||||
|
# Service account specific configs
|
||||||
|
SA_CONFIGS = {
|
||||||
|
"portfolio-agent": {
|
||||||
|
"projects": ["homelab", "portfolio"],
|
||||||
|
"visibility": "public",
|
||||||
|
"role": "portfolio-agent"
|
||||||
|
},
|
||||||
|
"memory-agent": {
|
||||||
|
"projects": ["*"],
|
||||||
|
"visibility": "private",
|
||||||
|
"role": "authenticated-user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if request.user.username in SA_CONFIGS:
|
||||||
|
cfg = SA_CONFIGS[request.user.username]
|
||||||
|
projects = cfg["projects"]
|
||||||
|
visibility = cfg["visibility"]
|
||||||
|
role = cfg["role"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"memory_projects": projects,
|
||||||
|
"memory_visibility": visibility,
|
||||||
|
"memory_role": role
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. `minio` scope (existing)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Already in authentik-provision.py
|
||||||
|
_POLICY_EXPR = """
|
||||||
|
# Check for explicit policy override in user attributes
|
||||||
|
explicit = request.user.attributes.get("minio_policy")
|
||||||
|
if explicit:
|
||||||
|
return {"policy": explicit}
|
||||||
|
|
||||||
|
# Group-based defaults
|
||||||
|
if request.user.ak_groups.filter(name="homelab-admins").exists():
|
||||||
|
return {"policy": "consoleAdmin"}
|
||||||
|
elif request.user.ak_groups.filter(name="minio-admins").exists():
|
||||||
|
return {"policy": "readwrite"}
|
||||||
|
else:
|
||||||
|
return {"policy": "readonly"}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Claim Contracts
|
||||||
|
|
||||||
|
Each service documents what JWT claims it expects. Authentik must provide them.
|
||||||
|
|
||||||
|
### API Gateway (`api.riotpiao.com`)
|
||||||
|
|
||||||
|
**Required Claims:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permissions": ["llm:inference", "..."] // Check against required permission
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enforcement:**
|
||||||
|
```go
|
||||||
|
// Gateway just checks if required permission is in claims
|
||||||
|
if !slices.Contains(claims.Permissions, "llm:inference") {
|
||||||
|
return 403
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Service (`memory.riotpiao.com`)
|
||||||
|
|
||||||
|
**Required Claims:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sub": "user-id", // Owner for conversation filtering
|
||||||
|
"permissions": ["memory:read", "..."], // Capability check
|
||||||
|
"memory_projects": ["homelab", "..."], // Project filter (or ["*"])
|
||||||
|
"memory_visibility": "public|private", // Max visibility level
|
||||||
|
"memory_role": "admin|user|..." // Built-in role (optional)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enforcement:**
|
||||||
|
```rust
|
||||||
|
// Memory service filters results based on claims
|
||||||
|
fn filter_results(results: Vec<Doc>, claims: &Claims) -> Vec<Doc> {
|
||||||
|
results.into_iter().filter(|doc| {
|
||||||
|
// Project check
|
||||||
|
let project_ok = claims.memory_projects.contains("*")
|
||||||
|
|| claims.memory_projects.contains(&doc.project);
|
||||||
|
|
||||||
|
// Visibility check
|
||||||
|
let visibility_ok = match claims.memory_visibility.as_str() {
|
||||||
|
"private" => true, // Can see everything
|
||||||
|
"public" => doc.visibility == "public",
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Owner check for conversations
|
||||||
|
let owner_ok = doc.resource_type != "conversation"
|
||||||
|
|| doc.owner == claims.sub;
|
||||||
|
|
||||||
|
project_ok && visibility_ok && owner_ok
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MinIO (`minio.riotpiao.com`)
|
||||||
|
|
||||||
|
**Required Claims:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"policy": "consoleAdmin|readwrite|readonly|<custom>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enforcement:**
|
||||||
|
- MinIO natively reads `policy` claim via `MINIO_IDENTITY_OPENID_CLAIM_NAME=policy`
|
||||||
|
- Looks up policy by name in MinIO's IAM policy store
|
||||||
|
- Built-in: `consoleAdmin`, `readwrite`, `readonly`, `writeonly`, `diagnostics`
|
||||||
|
- Custom policies created via `mc admin policy create`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Accounts
|
||||||
|
|
||||||
|
Service accounts are Authentik users with `type: service_account` and client credentials grant.
|
||||||
|
|
||||||
|
### Creating a Service Account
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In authentik-provision.py, add to SERVICES dict:
|
||||||
|
|
||||||
|
"portfolio-agent": {
|
||||||
|
"type": "service_account", # No redirect URIs
|
||||||
|
"grant_types": ["client_credentials"],
|
||||||
|
"client_secret_source": ("portfolio", "portfolio-oidc", "CLIENT_SECRET"),
|
||||||
|
"generate_if_missing": True,
|
||||||
|
"display_name": "Portfolio Agent",
|
||||||
|
"groups": ["llm-users", "memory-users"], # Group membership
|
||||||
|
"attributes": { # User attributes
|
||||||
|
"memory_projects": ["homelab", "portfolio"],
|
||||||
|
"memory_visibility": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Account Token Flow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Service requests token with client credentials
|
||||||
|
curl -X POST https://authentik.riotpiao.com/application/o/token/ \
|
||||||
|
-d "grant_type=client_credentials" \
|
||||||
|
-d "client_id=portfolio-agent" \
|
||||||
|
-d "client_secret=$SECRET" \
|
||||||
|
-d "scope=openid permissions memory"
|
||||||
|
|
||||||
|
# 2. Authentik returns JWT with claims
|
||||||
|
{
|
||||||
|
"access_token": "eyJ...",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 300
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Service uses token to call API
|
||||||
|
curl https://api.riotpiao.com/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer eyJ..."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Onboarding
|
||||||
|
|
||||||
|
### Admin Creates User
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. Create user via Authentik API
|
||||||
|
POST /api/v3/core/users/
|
||||||
|
{
|
||||||
|
"username": "alice",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"groups": ["llm-users", "memory-users"],
|
||||||
|
"attributes": {
|
||||||
|
"memory_projects": ["homelab"],
|
||||||
|
"memory_visibility": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Set password or send invite
|
||||||
|
POST /api/v3/core/users/{pk}/set_password/
|
||||||
|
or
|
||||||
|
POST /api/v3/stages/invitation/invitations/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grant Additional Access
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add to group (grants permissions via GROUP_PERMISSIONS mapping)
|
||||||
|
PATCH /api/v3/core/users/{pk}/
|
||||||
|
{
|
||||||
|
"groups": ["llm-users", "memory-users", "memory-admins"] # Added memory-admins
|
||||||
|
}
|
||||||
|
|
||||||
|
# Or update attributes for fine-grained scopes
|
||||||
|
PATCH /api/v3/core/users/{pk}/
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"memory_projects": ["homelab", "new-project"], # Added project
|
||||||
|
"memory_visibility": "private" # Upgraded visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Offboard User
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Deactivate (preserves audit trail)
|
||||||
|
PATCH /api/v3/core/users/{pk}/
|
||||||
|
{"is_active": false}
|
||||||
|
|
||||||
|
# Revoke all tokens immediately
|
||||||
|
POST /api/v3/core/users/{pk}/revoke_tokens/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Phase 1: Add Memory Claims ⬜
|
||||||
|
|
||||||
|
1. Add `memory` scope property mapping to `authentik-provision.py`
|
||||||
|
2. Add scope to `poimen-memory` OAuth provider
|
||||||
|
3. Update Memory service to read claims instead of local RBAC
|
||||||
|
|
||||||
|
### Phase 2: Service Accounts ⬜
|
||||||
|
|
||||||
|
1. Add service account creation to `authentik-provision.py`
|
||||||
|
2. Create `portfolio-agent` service account
|
||||||
|
3. Test client credentials flow
|
||||||
|
|
||||||
|
### Phase 3: MinIO Fine-Grained ⬜
|
||||||
|
|
||||||
|
1. Create custom MinIO policies (`portfolio-rw`, `memory-rw`)
|
||||||
|
2. Update `minio` scope mapping for service accounts
|
||||||
|
3. Test bucket-level access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Claim Reference
|
||||||
|
|
||||||
|
| Claim | Source | Services |
|
||||||
|
|-------|--------|----------|
|
||||||
|
| `sub` | User ID (hashed) | All (owner checks) |
|
||||||
|
| `groups` | Group membership | ArgoCD, Grafana |
|
||||||
|
| `permissions` | Computed from groups | API Gateway, Memory |
|
||||||
|
| `memory_projects` | User attr + group | Memory |
|
||||||
|
| `memory_visibility` | User attr + group | Memory |
|
||||||
|
| `memory_role` | User attr + group | Memory |
|
||||||
|
| `policy` | User attr + group | MinIO |
|
||||||
|
| `immich_role` | Group membership | Immich |
|
||||||
@@ -163,7 +163,10 @@ GROUPS_MAPPING_PK = groups_mapping["pk"]
|
|||||||
# foundation for a future short-lived federated-operator credential.
|
# foundation for a future short-lived federated-operator credential.
|
||||||
_PERMISSIONS_EXPR = """
|
_PERMISSIONS_EXPR = """
|
||||||
GROUP_PERMISSIONS = {
|
GROUP_PERMISSIONS = {
|
||||||
|
# Universal admin
|
||||||
"homelab-admins": ["*"],
|
"homelab-admins": ["*"],
|
||||||
|
|
||||||
|
# Service admin groups (full control)
|
||||||
"grafana-admins": ["grafana:read", "grafana:write"],
|
"grafana-admins": ["grafana:read", "grafana:write"],
|
||||||
"minio-admins": ["minio:read", "minio:write"],
|
"minio-admins": ["minio:read", "minio:write"],
|
||||||
"forgejo-admins": ["forgejo:read", "forgejo:write"],
|
"forgejo-admins": ["forgejo:read", "forgejo:write"],
|
||||||
@@ -174,9 +177,14 @@ GROUP_PERMISSIONS = {
|
|||||||
"llm-admins": ["llm:read", "llm:write", "llm:inference"],
|
"llm-admins": ["llm:read", "llm:write", "llm:inference"],
|
||||||
"paperless-admins": ["paperless:read", "paperless:write"],
|
"paperless-admins": ["paperless:read", "paperless:write"],
|
||||||
"immich-admins": ["immich:read", "immich:write"],
|
"immich-admins": ["immich:read", "immich:write"],
|
||||||
"poimen-memory-admins": ["poimen-memory:read", "poimen-memory:write"],
|
"poimen-memory-admins": ["memory:read", "memory:write", "memory:admin"],
|
||||||
"k8s-devops-admin": ["k8s:devops"],
|
"k8s-devops-admin": ["k8s:devops"],
|
||||||
"vault-service-api": ["vault:read", "vault:write"],
|
"vault-service-api": ["vault:read", "vault:write"],
|
||||||
|
|
||||||
|
# Capability groups (non-admin users)
|
||||||
|
"llm-users": ["llm:inference"],
|
||||||
|
"memory-users": ["memory:read"],
|
||||||
|
"memory-writers": ["memory:read", "memory:write"],
|
||||||
}
|
}
|
||||||
perms = set()
|
perms = set()
|
||||||
for group in request.user.groups.all():
|
for group in request.user.groups.all():
|
||||||
@@ -240,6 +248,69 @@ policy_mapping = get_or_create(
|
|||||||
)
|
)
|
||||||
POLICY_MAPPING_PK = policy_mapping["pk"]
|
POLICY_MAPPING_PK = policy_mapping["pk"]
|
||||||
|
|
||||||
|
# Memory service (Poimen) claims - fine-grained access control.
|
||||||
|
# Returns memory_projects, memory_visibility, memory_role based on:
|
||||||
|
# 1. User attributes (memory_projects, memory_visibility)
|
||||||
|
# 2. Group membership (homelab-admins, poimen-memory-admins)
|
||||||
|
# 3. Service account configs (portfolio-agent, etc.)
|
||||||
|
_MEMORY_EXPR = """
|
||||||
|
# Service account specific configs (checked first)
|
||||||
|
SA_CONFIGS = {
|
||||||
|
"portfolio-agent": {
|
||||||
|
"projects": ["homelab", "portfolio"],
|
||||||
|
"visibility": "public",
|
||||||
|
"role": "portfolio-agent"
|
||||||
|
},
|
||||||
|
"memory-agent": {
|
||||||
|
"projects": ["*"],
|
||||||
|
"visibility": "private",
|
||||||
|
"role": "authenticated-user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
username = request.user.username
|
||||||
|
if username in SA_CONFIGS:
|
||||||
|
cfg = SA_CONFIGS[username]
|
||||||
|
return {
|
||||||
|
"memory_projects": cfg["projects"],
|
||||||
|
"memory_visibility": cfg["visibility"],
|
||||||
|
"memory_role": cfg["role"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default from user attributes
|
||||||
|
projects = request.user.attributes.get("memory_projects", [])
|
||||||
|
visibility = request.user.attributes.get("memory_visibility", "public")
|
||||||
|
role = "user"
|
||||||
|
|
||||||
|
# Admin group overrides
|
||||||
|
if request.user.ak_groups.filter(name="homelab-admins").exists():
|
||||||
|
projects = ["*"]
|
||||||
|
visibility = "private"
|
||||||
|
role = "admin"
|
||||||
|
elif request.user.ak_groups.filter(name="poimen-memory-admins").exists():
|
||||||
|
# memory-admins get full visibility but respect project restrictions
|
||||||
|
visibility = "private"
|
||||||
|
role = "admin"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"memory_projects": projects if projects else [],
|
||||||
|
"memory_visibility": visibility,
|
||||||
|
"memory_role": role
|
||||||
|
}
|
||||||
|
""".strip()
|
||||||
|
memory_mapping = get_or_create(
|
||||||
|
"/api/v3/propertymappings/provider/scope/",
|
||||||
|
"/api/v3/propertymappings/provider/scope/",
|
||||||
|
"scope_name=memory",
|
||||||
|
{
|
||||||
|
"name": "homelab: memory service claims",
|
||||||
|
"scope_name": "memory",
|
||||||
|
"expression": _MEMORY_EXPR,
|
||||||
|
},
|
||||||
|
patch_existing={"expression": _MEMORY_EXPR},
|
||||||
|
)
|
||||||
|
MEMORY_MAPPING_PK = memory_mapping["pk"]
|
||||||
|
|
||||||
# Fetch the standard openid/email/profile mapping pks (shipped by default).
|
# Fetch the standard openid/email/profile mapping pks (shipped by default).
|
||||||
status, res = api("GET", "/api/v3/propertymappings/provider/scope/")
|
status, res = api("GET", "/api/v3/propertymappings/provider/scope/")
|
||||||
by_scope = {m["scope_name"]: m["pk"] for m in res["results"]}
|
by_scope = {m["scope_name"]: m["pk"] for m in res["results"]}
|
||||||
@@ -265,14 +336,17 @@ homelab_admins = get_or_create(
|
|||||||
# k8s-devops-admin is declared with no target yet - foundation for a future
|
# k8s-devops-admin is declared with no target yet - foundation for a future
|
||||||
# short-lived federated-operator credential.
|
# short-lived federated-operator credential.
|
||||||
SERVICE_ADMIN_GROUP_NAMES = [
|
SERVICE_ADMIN_GROUP_NAMES = [
|
||||||
|
# Service admin groups (full control of their service)
|
||||||
"grafana-admins", "minio-admins", "forgejo-admins", "homarr-admins",
|
"grafana-admins", "minio-admins", "forgejo-admins", "homarr-admins",
|
||||||
"portainer-admins", "kmsvc-admins", "temporal-admins", "llm-admins",
|
"portainer-admins", "kmsvc-admins", "temporal-admins", "llm-admins",
|
||||||
"paperless-admins", "immich-admins", "poimen-memory-admins",
|
"paperless-admins", "immich-admins", "poimen-memory-admins",
|
||||||
"k8s-devops-admin",
|
"k8s-devops-admin",
|
||||||
# Not a human-admin group like the others - Vault Identity Group aliasing
|
# Vault Identity Group aliasing target for service/API access
|
||||||
# target for service/API (non-browser) access to Vault, kept separate from
|
|
||||||
# homelab-admins' blanket "*" grant. See k8s/infra/iam/scripts/vault-provision.sh.
|
|
||||||
"vault-service-api",
|
"vault-service-api",
|
||||||
|
# Capability groups (non-admin users with specific permissions)
|
||||||
|
"llm-users", # Can call LLM inference, no admin
|
||||||
|
"memory-users", # Can query memory, no write
|
||||||
|
"memory-writers", # Can query and write to memory
|
||||||
]
|
]
|
||||||
service_admin_groups = {}
|
service_admin_groups = {}
|
||||||
for group_name in SERVICE_ADMIN_GROUP_NAMES:
|
for group_name in SERVICE_ADMIN_GROUP_NAMES:
|
||||||
@@ -439,11 +513,17 @@ SERVICES = {
|
|||||||
app_pks_for_binding = []
|
app_pks_for_binding = []
|
||||||
|
|
||||||
for name, cfg in SERVICES.items():
|
for name, cfg in SERVICES.items():
|
||||||
# MinIO also needs the "policy" claim (via the minio scope mapping) so its
|
# Service-specific scope mappings:
|
||||||
# MINIO_IDENTITY_OPENID_CLAIM_NAME=policy maps homelab-admins -> consoleAdmin.
|
# - MinIO: "policy" claim for MINIO_IDENTITY_OPENID_CLAIM_NAME
|
||||||
# Immich needs "immich_role" so its OAuth roleClaim can grant admin.
|
# - Immich: "immich_role" for OAuth roleClaim
|
||||||
provider_mappings = SCOPE_PKS + ([POLICY_MAPPING_PK] if name == "minio" else []) \
|
# - poimen-memory, local-llm: "memory" scope for fine-grained access
|
||||||
+ ([IMMICH_ROLE_MAPPING_PK] if name == "immich" else [])
|
provider_mappings = SCOPE_PKS[:]
|
||||||
|
if name == "minio":
|
||||||
|
provider_mappings.append(POLICY_MAPPING_PK)
|
||||||
|
if name == "immich":
|
||||||
|
provider_mappings.append(IMMICH_ROLE_MAPPING_PK)
|
||||||
|
if name in ("poimen-memory", "local-llm", "portfolio-agent"):
|
||||||
|
provider_mappings.append(MEMORY_MAPPING_PK)
|
||||||
ns, secret_name, key = cfg["client_secret_source"]
|
ns, secret_name, key = cfg["client_secret_source"]
|
||||||
client_secret = kubectl_get_secret_key(ns, secret_name, key)
|
client_secret = kubectl_get_secret_key(ns, secret_name, key)
|
||||||
if client_secret is None:
|
if client_secret is None:
|
||||||
@@ -640,7 +720,123 @@ app_pks_for_binding.append(("kubernetes", k8s_application["pk"]))
|
|||||||
print(f" kubernetes: provider pk={k8s_provider['pk']} application pk={k8s_application['pk']}")
|
print(f" kubernetes: provider pk={k8s_provider['pk']} application pk={k8s_application['pk']}")
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
print("[5/5] Binding homelab-admins to every application (guaranteed access for rock)...")
|
# Service accounts for programmatic API access (client_credentials flow).
|
||||||
|
# These are Authentik users with type=service_account, not OAuth applications.
|
||||||
|
# They authenticate via client_credentials grant to get JWTs with claims.
|
||||||
|
print("\n[SERVICE ACCOUNTS] Creating service accounts for API access...")
|
||||||
|
|
||||||
|
SERVICE_ACCOUNTS = {
|
||||||
|
"portfolio-agent": {
|
||||||
|
"groups": [], # Permissions come from SA_CONFIGS in memory scope mapping
|
||||||
|
"attributes": {
|
||||||
|
"memory_projects": ["homelab", "portfolio"],
|
||||||
|
"memory_visibility": "public",
|
||||||
|
},
|
||||||
|
"secret_ns": "portfolio",
|
||||||
|
"secret_name": "portfolio-agent-oidc",
|
||||||
|
},
|
||||||
|
"memory-agent": {
|
||||||
|
"groups": [], # Permissions come from SA_CONFIGS in memory scope mapping
|
||||||
|
"attributes": {
|
||||||
|
"memory_projects": ["*"],
|
||||||
|
"memory_visibility": "private",
|
||||||
|
},
|
||||||
|
"secret_ns": "poimen",
|
||||||
|
"secret_name": "memory-agent-oidc",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for sa_name, sa_cfg in SERVICE_ACCOUNTS.items():
|
||||||
|
# Check if secret already exists (don't regenerate credentials)
|
||||||
|
existing_secret = kubectl_get_secret_key(sa_cfg["secret_ns"], sa_cfg["secret_name"], "CLIENT_SECRET")
|
||||||
|
|
||||||
|
# Get or create the service account user
|
||||||
|
status, res = api("GET", f"/api/v3/core/users/?username={sa_name}")
|
||||||
|
if status != 200:
|
||||||
|
die(f"GET users for {sa_name} -> {status} {res}")
|
||||||
|
|
||||||
|
if res.get("results"):
|
||||||
|
sa_user = res["results"][0]
|
||||||
|
# Update attributes and groups
|
||||||
|
group_pks = [service_admin_groups[g]["pk"] for g in sa_cfg["groups"] if g in service_admin_groups]
|
||||||
|
status, sa_user = api("PATCH", f"/api/v3/core/users/{sa_user['pk']}/", {
|
||||||
|
"attributes": sa_cfg["attributes"],
|
||||||
|
"groups": group_pks,
|
||||||
|
"is_active": True,
|
||||||
|
})
|
||||||
|
if status not in (200, 201):
|
||||||
|
die(f"PATCH service account {sa_name} -> {status} {sa_user}")
|
||||||
|
print(f" {sa_name}: updated existing service account")
|
||||||
|
else:
|
||||||
|
# Create new service account user
|
||||||
|
group_pks = [service_admin_groups[g]["pk"] for g in sa_cfg["groups"] if g in service_admin_groups]
|
||||||
|
status, sa_user = api("POST", "/api/v3/core/users/", {
|
||||||
|
"username": sa_name,
|
||||||
|
"name": f"Service Account: {sa_name}",
|
||||||
|
"type": "service_account",
|
||||||
|
"path": "service-accounts",
|
||||||
|
"is_active": True,
|
||||||
|
"attributes": sa_cfg["attributes"],
|
||||||
|
"groups": group_pks,
|
||||||
|
})
|
||||||
|
if status not in (200, 201):
|
||||||
|
die(f"POST service account {sa_name} -> {status} {sa_user}")
|
||||||
|
print(f" {sa_name}: created new service account")
|
||||||
|
|
||||||
|
# Create OAuth provider for this service account (client_credentials flow)
|
||||||
|
sa_client_secret = existing_secret or gen_secret(40)
|
||||||
|
sa_provider = get_or_create(
|
||||||
|
"/api/v3/providers/oauth2/", "/api/v3/providers/oauth2/",
|
||||||
|
f"name={sa_name}",
|
||||||
|
{
|
||||||
|
"name": sa_name,
|
||||||
|
"client_id": sa_name,
|
||||||
|
"client_secret": sa_client_secret,
|
||||||
|
"client_type": "confidential",
|
||||||
|
"authorization_flow": AUTHORIZATION_FLOW_PK,
|
||||||
|
"invalidation_flow": INVALIDATION_FLOW_PK,
|
||||||
|
"signing_key": SIGNING_KEY_PK,
|
||||||
|
"property_mappings": SCOPE_PKS + [MEMORY_MAPPING_PK],
|
||||||
|
"sub_mode": "hashed_user_id",
|
||||||
|
"include_claims_in_id_token": True,
|
||||||
|
"grant_types": ["client_credentials"],
|
||||||
|
"redirect_uris": [], # No redirects for client_credentials
|
||||||
|
},
|
||||||
|
patch_existing={
|
||||||
|
"property_mappings": SCOPE_PKS + [MEMORY_MAPPING_PK],
|
||||||
|
"grant_types": ["client_credentials"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create application for the service account
|
||||||
|
sa_application = get_or_create(
|
||||||
|
"/api/v3/core/applications/", "/api/v3/core/applications/",
|
||||||
|
f"slug={sa_name}&superuser_full_list=true",
|
||||||
|
{
|
||||||
|
"name": f"Service Account: {sa_name}",
|
||||||
|
"slug": sa_name,
|
||||||
|
"provider": sa_provider["pk"],
|
||||||
|
"meta_launch_url": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
app_pks_for_binding.append((sa_name, sa_application["pk"]))
|
||||||
|
|
||||||
|
# Store credentials in k8s Secret
|
||||||
|
if not existing_secret:
|
||||||
|
kubectl_create_secret(sa_cfg["secret_ns"], sa_cfg["secret_name"], {
|
||||||
|
"CLIENT_ID": sa_name,
|
||||||
|
"CLIENT_SECRET": sa_client_secret,
|
||||||
|
"TOKEN_URL": "https://authentik.riotpiao.com/application/o/token/",
|
||||||
|
"ISSUER": f"https://authentik.riotpiao.com/application/o/{sa_name}/",
|
||||||
|
})
|
||||||
|
print(f" {sa_name}: generated credentials -> {sa_cfg['secret_ns']}/{sa_cfg['secret_name']}")
|
||||||
|
else:
|
||||||
|
print(f" {sa_name}: reusing existing credentials from {sa_cfg['secret_ns']}/{sa_cfg['secret_name']}")
|
||||||
|
|
||||||
|
print(f" {sa_name}: provider pk={sa_provider['pk']} application pk={sa_application['pk']}")
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
print("\n[5/5] Binding homelab-admins to every application (guaranteed access for rock)...")
|
||||||
for name, app_pk in app_pks_for_binding:
|
for name, app_pk in app_pks_for_binding:
|
||||||
get_or_create(
|
get_or_create(
|
||||||
"/api/v3/policies/bindings/", "/api/v3/policies/bindings/",
|
"/api/v3/policies/bindings/", "/api/v3/policies/bindings/",
|
||||||
@@ -667,6 +863,10 @@ SERVICE_GROUP_TO_APP_SLUG = {
|
|||||||
"paperless-admins": "paperless",
|
"paperless-admins": "paperless",
|
||||||
"immich-admins": "immich",
|
"immich-admins": "immich",
|
||||||
"llm-admins": "local-llm",
|
"llm-admins": "local-llm",
|
||||||
|
"llm-users": "local-llm",
|
||||||
|
"poimen-memory-admins": "poimen-memory",
|
||||||
|
"memory-users": "poimen-memory",
|
||||||
|
"memory-writers": "poimen-memory",
|
||||||
}
|
}
|
||||||
for group_name, app_slug in SERVICE_GROUP_TO_APP_SLUG.items():
|
for group_name, app_slug in SERVICE_GROUP_TO_APP_SLUG.items():
|
||||||
app_pk = next((pk for n, pk in app_pks_for_binding if n == app_slug), None)
|
app_pk = next((pk for n, pk in app_pks_for_binding if n == app_slug), None)
|
||||||
|
|||||||
Reference in New Issue
Block a user