Initial: Tibi-Doku auf Starlight-Basis (Nova-Theme, Docker-Stack, Sync-Skripte)

This commit is contained in:
apairon committed 2026-10-16 11:43:04 +00:00
commit 58502aa3c8
78 files changed
+22860

No files matched your search

@@ -0,0 +1,606 @@
---
title: 'Authentication & Authorization'
---
# Authentication & Authorization
## Overview
Tibi-Server uses JWT-based authentication with a role and permission system. Access control is configured per collection via permission sets that support public access, role-based access, token-based access, and custom permissions.
## Login Flow
### Username/Password Login
```
POST /api/v1/login
Content-Type: application/json
{"username": "admin", "password": "admin"}
```
1. Password is hashed with MD5 and compared against the stored hash
2. If matched, a JWT token and refresh token are generated
3. JWT token is returned in the response body
4. Refresh token is set as an HttpOnly cookie
### Refresh Token Login
```
POST /api/v1/login
Cookie: tibi-refresh-token=eyJ...
```
When no username/password is provided but a refresh token cookie exists:
1. The refresh token JWT is validated (HMAC-SHA256, expiration required)
2. User ID is extracted from the token claims
3. User is looked up by ID
4. New JWT token is issued
### Logout
```
POST /api/v1/login?logout=1
```
Clears the refresh token cookie (sets expiry to -1). Any non-empty `logout` query parameter value triggers logout.
### Impersonation (Admin/OrgAdmin → User)
```
POST /api/v1/login/impersonate
X-Auth-Token: <admin-jwt>
Content-Type: application/json
{"userId": "<target-user-id>"}
```
Allows a SystemAdmin (role=0) or OrgAdmin to obtain a JWT token acting as another user.
This is useful for debugging permission issues ("why can User X not see document Y?").
**Security rules:**
| Role | Can impersonate | Reason |
|---|---|---|
| SystemAdmin (0) | Any user | Admin already has full access |
| OrgAdmin | Users in same org(s) | OrgAdmin can already grant themselves the same team permissions |
| User (1) | Nobody | Would break the permission model |
**Response (200):**
```json
{
"token": "<jwt-acting-as-target-user>",
"user": { "id": "...", "username": "targetuser", "role": 1, ... },
"impersonatedBy": {
"id": "<original-admin-id>",
"username": "admin",
"role": 0
}
}
```
**Important:** The JWT token contains an `impersonatedBy` field in the `object` claim,
which is used by the audit system to record who actually performed each action.
Every audit entry during an impersonated session will include:
- `userId`/`username`/`userRole` → the impersonated (target) user
- `impersonatedBy` → the original admin who initiated the impersonation
- `authMethod` → `"impersonation"`
**Logout from impersonation:** Same as normal logout (`POST /api/v1/login?logout=1`).
The refresh token cookie is cleared, ending the impersonated session. The admin
must re-authenticate with their own credentials.
**Chained impersonation:** If an admin impersonates an org-admin, and that
org-admin then impersonates a user, the `impersonatedBy` field always points to
the **original admin** who started the chain — not the intermediate impersonator.
**Errors:**
| Status | Description |
|---|---|
| 400 | `userId is required` or invalid userId format |
| 403 | `impersonation not allowed: insufficient role` |
| 403 | `impersonation not allowed: not admin of target user's org` |
| 404 | target user not found |
## JWT Token Structure
### Access Token
| Field | Value | Description |
|-------|-------|-------------|
| Algorithm | HS256 | HMAC-SHA256 |
| `object` | User data | `{id, username, role, permissions, orgs, teams, primaryOrgId, meta, ...}` |
| `type` | `"User"` | Token type identifier |
| `exp` | now + 60min | Expiration timestamp |
The `password` field is explicitly excluded from the token (via custom JSON marshaling).
The `object` includes org and team membership if the user has any:
```json
{
"object": {
"id": "60a1b2c3d4...",
"username": "alice",
"role": 1,
"permissions": [],
"orgs": [{"orgId": "...", "role": "admin"}],
"teams": [{"teamId": "...", "role": "member"}],
"primaryOrgId": "..."
},
"type": "User",
"exp": 1700000000
}
```
See [Orgs & Teams](18-orgs-teams.md) for details on org/team-based access control.
**Auth Header:**
```
X-Auth-Token: eyJhbGciOiJIUzI1NiIs...
```
### Refresh Token
| Field | Value | Description |
|-------|-------|-------------|
| Algorithm | HS256 | HMAC-SHA256 |
| `object` | `{id: "userId"}` | Minimal user reference |
| `type` | `"RefreshTokenData"` | Token type identifier |
| `exp` | now + 10h | Expiration timestamp |
**Cookie:**
| Property | Value |
|----------|-------|
| Name | `tibi-refresh-token` |
| MaxAge | 36000 (10 hours) |
| Path | `/` |
| HttpOnly | `true` |
| SameSite | `Strict` |
| Secure | `false` |
### Token Validation
Token validation includes caching — subsequent calls within the same request reuse the validated result:
```go
// Cached in gin.Context:
// "validate-auth-token-object" → user data map
// "validate-auth-token-type" → token type string
// "validate-auth-token-err" → validation error
```
---
## Admin Tokens (Server-Level)
Admin tokens provide token-based access for agents, CI/CD pipelines, and other automated systems without requiring the JWT login flow.
### Configuration
```yaml
api:
adminTokens:
- token: "agent-token-xyz"
label: "ci-deploy" # Optional: human-readable label for audit logs
permissions:
- project # Shorthand: all project CRUD operations
- project.reload # Reload project config
- user # Shorthand: all user CRUD operations
- server.shutdown
- namespace.myproject # All collections in myproject
- token: "readonly-token"
label: "monitoring" # Appears as tokenLabel in audit entries
permissions:
- project.read
- user.read
- namespace.*.*.read # Read-only across all namespaces and collections
- token: "importer-token"
label: "data-import"
permissions:
- namespace.myproject.articles.create # Only create in myproject/articles
- namespace.myproject.articles.update # Only update in myproject/articles
```
### Available Permissions
| Permission | Description |
|-----------|-------------|
| `project` | Shorthand for all project operations (`project.*`) |
| `project.read` | GET projects |
| `project.create` | POST (create) projects |
| `project.update` | PUT (update) projects |
| `project.delete` | DELETE projects |
| `project.reload` | Reload project configuration |
| `user` | Shorthand for all user operations (`user.*`) |
| `user.read` | GET users |
| `user.create` | POST (create) users |
| `user.update` | PUT (update) users |
| `user.delete` | DELETE users |
| `server.shutdown` | Shutdown the server |
| `namespace` | Shorthand: all collections in all projects |
| `namespace.{project}` | Shorthand: all collections in a project |
| `namespace.{project}.{collection}` | Shorthand: all actions on a collection |
| `namespace.{project}.{collection}.read` | GET documents |
| `namespace.{project}.{collection}.create` | POST (create) documents |
| `namespace.{project}.{collection}.update` | PUT (update) documents |
| `namespace.{project}.{collection}.delete` | DELETE documents |
### Wildcards
Use `*` as a wildcard for any single segment in a permission string:
| Permission | Description |
|-----------|-------------|
| `namespace.*` | All collections in any project |
| `namespace.*.articles.read` | Read articles across all projects |
| `namespace.myproject.*.read` | Read all collections in myproject |
| `namespace.*.*.read` | Read-only across everything |
Shorthand works at any level — a permission with fewer segments implies all descendants:
- `namespace` = `namespace.*.*.*` (everything)
- `namespace.myproject` = `namespace.myproject.*.*` (all collections, all actions)
- `namespace.myproject.articles` = `namespace.myproject.articles.*` (all actions)
### Usage
Send the token via `X-Admin-Token` header or `Authorization: Bearer` header:
```
# Via X-Admin-Token header — project management
POST /api/v1/project
X-Admin-Token: agent-token-xyz
Content-Type: application/json
{"name": "my-project", "description": "A new project", "configFile": "/path/to/config.yml"}
```
```
# Via Authorization: Bearer header — collection access
GET /api/v1/_/myproject/articles
Authorization: Bearer readonly-token
```
```
# Create document in a collection
POST /api/v1/_/myproject/articles
X-Admin-Token: importer-token
Content-Type: application/json
{"title": "New Article", "body": "Content..."}
```
### Token Priority
For project/user endpoints:
1. **Admin Token** (`X-Admin-Token` / `Authorization: Bearer`): Checked first. If present and valid, access is granted immediately. If present but invalid, access is denied (no fallthrough).
2. **JWT Auth** (`X-Auth-Token`): Checked as fallback if no admin token is provided.
For collection endpoints (`/_/:project/:collection`):
1. **Admin Token**: If present, checked for `namespace.{project}.{collection}.{action}` permission. If present but lacking permission, access is denied (no fallthrough to JWT or public permissions).
2. **Collection Token Permissions**: Header/query-based tokens defined in collection config.
3. **Public / User / JWT**: Normal permission chain if no admin token is present.
### Project Admin Endpoints
For project admin endpoints (`reload`, `regenerate-search`, `backup`, `restore`), three auth methods are supported (checked in order):
1. Server-level admin tokens with the corresponding permission (e.g. `project.reload`, `project.regenerate-search`, `project.backup`)
2. Project-level tokens configured in the project's `config.yml` (`admin.tokens[].allowReload`, `admin.tokens[].allowRegenerateSearch`, `admin.tokens[].allowBackup`)
3. JWT login with admin role (`X-Auth-Token` header)
### Audit Trail
All admin token actions are logged in the audit log with additional context:
| Field | Description | Example |
|-------|-------------|---------|
| `authMethod` | Authentication method used | `admin-token`, `jwt` |
| `tokenLabel` | Label from token config (if set) | `ci-deploy` |
| `tokenPrefix` | First 8 characters of the token | `agent-to` |
| `ip` | Client IP address | `192.168.1.100` |
The full token is **never** logged for security reasons. Use descriptive `label` values to identify tokens in audit logs.
---
## Roles
| Role | Value | Description |
|------|-------|-------------|
| Admin | `0` | Full access to all system and collection endpoints |
| User | `1` | Access limited to assigned projects and permitted methods |
### Admin Role Behavior
- Can access all projects regardless of assignment
- Can manage all users (create, update, delete)
- Can manage all projects
- Bypasses all collection permission checks
- No projection restrictions
- No pre-filter application
- Only admins can change `role`, `permissions`, and `llmBudgets` on users
### User Role Behavior
- Can only see projects belonging to their orgs (via `project.orgId` ∈ user's org IDs)
- Cannot create/update/delete projects (OrgAdmins can modify `teams[]`)
- Can only GET and PUT their own user profile (OrgAdmins have broader scope)
- Cannot change `username`, `role`, `permissions`, or `llmBudgets`
- Subject to collection permission checks (via team permissions)
- See [Orgs & Teams](18-orgs-teams.md) for the full role hierarchy
---
## Permission System
Each collection defines a `permissions` map with named permission sets:
```yaml
permissions:
public:
methods:
get: true
filter:
published: true
user:
methods:
get: true
post: true
put:
allow: true
bulk: true # allow bulk PUT
delete:
allow: true
bulk: true # allow bulk DELETE
"token:apikey":
methods:
get: true
post: true
editor:
methods:
get: true
put: true # shorthand for {allow: true}
```
### PermissionSet Structure
```go
type PermissionSet struct {
Methods map[string]MethodPermission // Allowed methods (get, post, put, delete)
Filter dbcrud.M // Auto-applied filter for queries
ValidProjections []string // Allowed projection names
CompiledFilterSubs []*CompiledSubst // Pre-compiled $subst expressions
}
// MethodPermission represents a method permission.
// Can be unmarshalled from a boolean (true/false) or an object {allow: bool, bulk: bool}.
type MethodPermission struct {
Allow bool // Allow the method
Bulk bool // Allow bulk operations (POST array, PUT/DELETE without ID)
}
```
### Permission Check Priority
When a request comes in, permissions are checked in this order:
```
1. Token-based permissions (header/query parameter match)
│
├── Token name found, value matches → Use token's permission set
├── Token name found, value WRONG → ACCESS DENIED (no fallback!)
└── No token name found → Continue
│
2. Public permissions
│
├── Method allowed → ACCESS GRANTED (no auth required)
└── Method not allowed → Continue
│
3. Auth token validation (X-Auth-Token header)
│
├── No token / invalid → ACCESS DENIED
└── Valid token → Continue
│
4. Admin role check (role = 0)
│
├── Is admin → ACCESS GRANTED
└── Not admin → Continue
│
5. Team "user" permissions (role = 1, team-assigned to project)
│
├── User is member of a team assigned to this project
│ AND collection has "user" permission set → Use "user" set
└── Otherwise → Continue
│
6. Custom user permissions
│
├── Auth permissions contain "namespace:permName" → Use that permission set
└── No match → Continue
│
7. Custom team permissions
│
├── User's teams (assigned to project) have permissions matching a collection key
│ → Use that team permission set (union of all matching teams)
└── No match → ACCESS DENIED
```
See [Orgs & Teams — Permission System](18-orgs-teams.md#permission-system) for details on team-based permission resolution.
### Token-Based Permissions
Token permissions are matched against HTTP headers or query parameters:
```yaml
permissions:
"token:secretkey123": # Matches ?token=secretkey123 OR header token: secretkey123
methods:
get: true
"x-api-key:my-api-key": # Matches ?x-api-key=my-api-key OR header x-api-key: my-api-key
methods:
get: true
post: true
```
**Important:** The permission key format is `name:value`. The `name` part is checked against both query parameters and headers. If the `name` is found but the `value` doesn't match, access is **denied immediately** with no fallback to public/user permissions.
### Custom Permissions
Users can have custom permission strings in their `permissions` array:
```json
{
"username": "editor1",
"role": 1,
"permissions": ["myblog:editor", "myblog:moderator"]
}
```
These are matched against collection permission keys using the format `{namespace}:{permissionKey}`:
```yaml
# In project config (namespace: myblog)
permissions:
editor: # Matched by "myblog:editor"
methods:
get: true
put: true
moderator: # Matched by "myblog:moderator"
methods:
get: true
delete: true
```
---
## Pre-Filters
Permission sets can define filters that are automatically applied to all queries:
```yaml
permissions:
public:
methods:
get: true
filter:
published: true
deletedAt: null
user:
methods:
get: true
post: true
put: true
filter:
author:
$subst:
expr: "auth.id" # Only see own documents
```
### How Pre-Filters Work
1. The permission set's filter is deep-copied (to avoid mutation)
2. `$subst` expressions are resolved with the current request context
3. The filter is combined with any user-provided filter query via `$and`
4. Admin role (0) never has pre-filters applied
### Filter Combination
```
Final filter = $and: [
{ _id: <from URL if single document> },
{ <pre-filter from permissions> },
{ <user-provided filter query> }
]
```
---
## Projection Restrictions
Permission sets can restrict which projections are available:
```yaml
permissions:
public:
methods:
get: true
validProjections:
- summary # Public can only use "summary" projection
user:
methods:
get: true
validProjections:
- summary
- detailed # Users can also use "detailed"
```
**Behavior:**
- If `validProjections` is empty/not set → no restrictions (any projection or none)
- If `validProjections` is set and no `projection` parameter → first entry is used as default
- If `validProjections` is set and invalid projection requested → `400 Bad Request`
- Admin role (0) is never restricted
---
## AuthData Model
Extracted from the JWT token on each authenticated request:
```go
type AuthData struct {
ID string // User ObjectID (hex)
Username string // Username
Role int // 0=Admin, 1=User
Permissions []string // Custom permission strings
}
```
---
## User Model
```go
type User struct {
Model // ID, InsertTime, UpdateTime
Username *string // Required, alphanumeric, unique
Password *string // Required, min 3 chars (stored as MD5 hash)
Role *int // 0=Admin, 1=User (validated min=0, max=2)
Permissions []string // Custom permission strings
Meta map[string]interface{} // Arbitrary user metadata
LLMBudgets []LLMBudget // Per-provider LLM token budgets
}
```
### User Search
The `_search` parameter on `/api/v1/user` searches across:
- `username`
- `meta.company`
- `meta.prename`
- `meta.surname`
- `meta.email`
- `meta.phone`
---
## Server-Level Admin Authentication
Server-level admin endpoints (e.g. shutdown) are protected by admin tokens with the appropriate permission:
```bash
curl -X POST http://localhost:8080/api/v1/admin/shutdown \
-H "X-Admin-Token: your-admin-token"
```
Admin tokens are configured via `api.adminTokens` in `config.yml`. See [Configuration](02-configuration.md) for details.
## Personal Access Tokens (PAT)
Users can create and manage their own API tokens. A PAT uses the same
`X-Auth-Token` header as the JWT and is always checked live against the owner's
current rights. Tokens are a core feature and always enabled.
See [22-personal-access-tokens.md](22-personal-access-tokens.md) for format,
scope grammar, endpoints, examples and security properties.