Adapt Specification: Authentication and Security
Status: The project maintains this document as an implementation specification. If the running code and this document differ, the code on
mainwins. This is not a roadmap or the authoritative user documentation. See the documentation contract and user manual.
1. Authentication and Authorization System
Purpose
Provide secure, multi-user access control for all Adapt resources.
Architecture
The role-based access control system has six main components:
- Authentication layer - Login creates a database session and an
adapt_sessioncookie. - User and Group Management - Groups organize users for permission inheritance.
- Permission System - Groups have resource-level permissions (read or write).
- Enforcement layer - Generated resource routes require authentication and a matching resource permission.
- API key system - The
X-API-Keyheader supports programmatic access. - Audit system - Authentication, administrative actions, and successful dataset mutations create audit records.
Database Schema
adapt/storage.py defines these SQLModel tables:
| Table | Purpose | Important constraints |
|---|---|---|
users |
User accounts | Unique username |
groups |
Permission groups | Unique name |
usergroup |
User and group links | Composite primary key. Foreign keys reference users.id and groups.id. oidc_managed marks memberships that OIDC may remove on later sync. |
permission |
Resource actions | Unique pair of resource and action |
grouppermission |
Group and permission links | Composite primary key. Foreign keys reference groups.id and permission.id. |
dbsession |
Browser sessions | Unique token. user_id references users.id. Nullable id_token for Keycloak logout. |
apikey |
Hashed API keys | Unique key_hash. user_id references users.id. |
auditlog |
Audit events | Nullable user_id and nullable resource |
lock_records |
Resource write locks | Unique indexed resource |
The Action enum limits permission actions to read and write.
The plugin cache is separate from the SQLModel definitions. adapt/cache.py
creates a SQLite cache table with key, value, expires_at, resource,
and user columns. The search subsystem creates its own SQLite tables.
Authentication Flow
Session-Based (Browser)
- Submit credentials to
POST /auth/login. - The route compares the password with its PBKDF2 hash.
- The route rejects an inactive user.
- The route creates a seven-day database session.
- The route sets the HttpOnly
adapt_sessioncookie. - The authentication middleware resolves this cookie for later requests.
- The resolver rejects the session if the user is inactive.
- Each valid request extends the session expiration by seven days.
API Key-Based (Programmatic)
- Include the
X-API-Key: <key>header in the request. - The authentication dependency computes the SHA-256 hash.
- The dependency finds an active, unexpired key with this hash.
- The dependency rejects the key if its user is inactive.
- The dependency returns the associated user and updates
last_used_at.
OIDC Bearer (REST and MCP)
- Include
Authorization: Bearer <jwt>whenoidc.issuerandoidc.client_idare set. - The resolver validates signature against Keycloak JWKS, then
iss,aud, andexp. - The resolver creates or updates the user from
preferred_username. - The resolver syncs
oidc_managedgroup memberships andadapt-adminsuperuser status. - Inactive users are rejected.
API Key Management
- Self-issue: Authenticated users can create their own keys through
POST /api/apikeysor the Profile UI. - Expiration: Keys can have an optional expiration of up to one year.
- Revocation: Users can revoke their own keys through the
/api/apikeys/{id}DELETE endpoint or the Profile UI. - Security: Adapt generates keys securely, hashes them for storage, and never allows retrieval after creation.
- Audit: Successful key creation and revocation create audit records.
Permission Checking
For each protected route:
- Resolve the user from the session cookie, API key, or Bearer JWT.
- If the user is a superuser, permit the action.
- Query permissions through the user group membership:
sql SELECT permission.* FROM permission JOIN grouppermission ON grouppermission.permission_id = permission.id JOIN usergroup ON usergroup.group_id = grouppermission.group_id WHERE usergroup.user_id = ? AND permission.resource = ? AND permission.action = ? - If no matching permission exists, return
403.
Automatic Enforcement
All dynamically generated routes (/api/*, /ui/*, /schema/*) are protected via FastAPI dependency injection:
app.include_router(
router,
prefix=full_prefix,
dependencies=[Depends(permission_dependency("auto", namespace))]
)
The permission_dependency function resolves a session, API key, or Bearer JWT. It maps
GET to read and unsafe methods to write. It returns 403 when permission
is denied.
Security Features
- Password Hashing: PBKDF2 hashing uses 100,000 iterations and a per-user salt.
- Session Expiration: Sessions have a 7-day TTL. Adapt checks this on every request.
- Session Cleanup: A background task removes expired sessions daily.
- Sliding Session Renewal: Active sessions extend automatically. Each request updates
last_active. - Password Changes: Users can change their password after current-password verification. Superusers can reset passwords. Each change revokes all browser sessions for the affected user.
- HttpOnly cookies: JavaScript cannot read the session cookie.
- Secure cookies: Direct TLS through
adapt serveenables the Secure flag. - SameSite=Lax: The session cookie uses this browser policy.
- CSRF: Unsafe cookie-authenticated requests require a matching CSRF cookie and header. API-key-only and Bearer-only requests are exempt.
- Constant-time comparison: Password verification uses
secrets.compare_digest. - Secure by Default: No permission means no access.
- Superuser Bypass: Superusers get emergency access.
- Audit Logging: Adapt records authentication, administrative changes, and successful dataset mutations.
- Row-Level Filtering: Plugins can filter rows during reads. Built-in plugins do not do so, and the hook does not safely enforce write-level RLS.
- Inactive-user enforcement: Login, session, API-key, and Bearer
authentication require
User.is_active. Deactivation also revokes browser sessions.
Runtime Behavior Locations
adapt/auth/password.pyhashes and compares passwords.adapt/auth/session.pycreates, resolves, and extends sessions.adapt/auth/dependencies.pyresolves users and checks permissions.adapt/api_keys.pycreates, resolves, and revokes API keys.adapt/auth/oidc.pyvalidates JWTs, runs JIT user and group sync, and builds OIDC redirects.adapt/auth/routes.pyprovides login, logout, OIDC callback, profile, password-change, and self-service key routes.adapt/admin/provides the administrative routes. These routes include user status changes.adapt/users.pychanges user status and revokes sessions during deactivation.adapt/audit.pycreates audit records.adapt/app.pyconfigures middleware and session cleanup.
Foreign Key ON DELETE behavior
- Deletion of a user cascades to
usergroup,dbsession, andapikeyrows. - Deletion of a group cascades to
usergroupandgrouppermissionrows. - Deletion of a permission cascades to
grouppermissionrows. - Deletion of an audit user sets
auditlog.user_idto null.
Row-Level Filtering Extension Point
- Interface:
Pluginincludesfilter_for_user(self, resource, user, rows). - Read behavior: The Dataset Engine passes raw rows through this method before serialization and query-parameter filtering.
- Default behavior: The base implementation returns every row, and no built-in plugin overrides it.
- Write limitation: The shared write path does not safely enforce this filter for mutation authorization. Plugins must implement a separate, tested write policy before claiming write-level RLS.
Current Audit Coverage
Audit entries cover successful login and logout. They cover API-key creation and revocation. They cover password changes and administrator password resets. They also cover these administrative changes:
- User and group creation or deletion
- User activation and deactivation
- Group membership changes
- Permission creation, deletion, and group assignment changes
- Manual lock operations
- Cache entry deletion and cache clearing
- Successful dataset creation, update, and deletion operations
Dataset mutations use create_dataset_rows, update_dataset_row, and
delete_dataset_row actions. REST and MCP writes use the same audit path.
The resource identifies the dataset path and optional sheet namespace. The
details identify the row count or row ID without copying dataset values.
Other unlisted writes do not create audit records.