feat(auth): add authentication, RBAC, API tokens, and default admin
Add a complete authentication and authorization system to Silo with three pluggable backends (local bcrypt, LDAP/FreeIPA, OIDC/Keycloak), session management, API token support, and role-based access control. Authentication backends: - Local: bcrypt (cost 12) password verification against users table - LDAP: FreeIPA simple bind with group-to-role mapping - OIDC: Keycloak redirect flow with realm role mapping - Backends are tried in order; users upserted to DB on first login Session and token management: - PostgreSQL-backed sessions via alexedwards/scs + pgxstore - Opaque API tokens (silo_ prefix, SHA-256 hashed, shown once) - 24h session lifetime, HttpOnly/SameSite=Lax/Secure cookies Role-based access control (admin > editor > viewer): - RequireAuth middleware: Bearer token -> session -> redirect/401 - RequireRole middleware: per-route-group minimum role enforcement - CSRF protection via justinas/nosurf on web forms, API exempt - CORS locked to configured origins when auth enabled Route restructuring: - Public: /health, /ready, /login, /auth/oidc, /auth/callback - Web (auth + CSRF): /, /projects, /schemas, /settings - API read (viewer): GET /api/** - API write (editor): POST/PUT/PATCH/DELETE /api/** User context wiring: - created_by/updated_by columns on items, projects, relationships - All create/update handlers populate tracking fields from context - CSV and BOM import handlers pass authenticated username - Revision creation tracks user across all code paths Default admin account: - Configurable via auth.local.default_admin_username/password - Env var overrides: SILO_ADMIN_USERNAME, SILO_ADMIN_PASSWORD - Idempotent: created on first startup, skipped if exists CLI and FreeCAD plugin: - silo token create/list/revoke subcommands (HTTP API client) - FreeCAD SiloClient sends Bearer token on all requests - Token read from ApiToken preference or SILO_API_TOKEN env var Web UI: - Login page (Catppuccin Mocha themed, OIDC button conditional) - Settings page with account info and API token management - User display name, role badge, and logout button in header - One-time token display banner with copy-to-clipboard Database (migration 009): - users table with role, auth_source, oidc_subject, password_hash - api_tokens table with SHA-256 hash, prefix, expiry, scopes - sessions table (scs pgxstore schema) - audit_log table (schema ready for future use) - created_by/updated_by ALTER on items, relationships, projects New dependencies: scs/v2, scs/pgxstore, go-oidc/v3, go-ldap/v3, justinas/nosurf, golang.org/x/oauth2
This commit is contained in:
257
docs/AUTH_USER_GUIDE.md
Normal file
257
docs/AUTH_USER_GUIDE.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Silo Authentication User Guide
|
||||
|
||||
## Logging In
|
||||
|
||||
### Username and Password
|
||||
|
||||
Navigate to the Silo web UI. If authentication is enabled, you'll be redirected to the login page.
|
||||
|
||||
Enter your username and password. This works for both local accounts and LDAP/FreeIPA accounts — Silo tries local authentication first, then LDAP if configured.
|
||||
|
||||
### Keycloak / OIDC
|
||||
|
||||
If your deployment has OIDC enabled, the login page will show a "Sign in with Keycloak" button. Click it to be redirected to your identity provider. After authenticating there, you'll be redirected back to Silo with a session.
|
||||
|
||||
## Roles
|
||||
|
||||
Your role determines what you can do in Silo:
|
||||
|
||||
| Role | Permissions |
|
||||
|------|-------------|
|
||||
| **viewer** | Read all items, projects, schemas, BOMs. Manage own API tokens. |
|
||||
| **editor** | Everything a viewer can do, plus: create/update/delete items, upload files, manage BOMs, import CSV, generate part numbers. |
|
||||
| **admin** | Everything an editor can do, plus: user management (future), configuration changes. |
|
||||
|
||||
Your role is shown as a badge next to your name in the header. For LDAP and OIDC users, the role is determined by group membership or token claims and re-evaluated on each login.
|
||||
|
||||
## API Tokens
|
||||
|
||||
API tokens allow the FreeCAD plugin, scripts, and CI pipelines to authenticate with Silo without a browser session. Tokens inherit your role.
|
||||
|
||||
### Creating a Token (Web UI)
|
||||
|
||||
1. Click **Settings** in the navigation bar
|
||||
2. Under **API Tokens**, enter a name (e.g., "FreeCAD workstation") and click **Create Token**
|
||||
3. The raw token is displayed once — copy it immediately
|
||||
4. Store the token securely. It cannot be shown again.
|
||||
|
||||
### Creating a Token (CLI)
|
||||
|
||||
```sh
|
||||
export SILO_API_URL=https://silo.kindred.internal
|
||||
export SILO_API_TOKEN=silo_<your-existing-token>
|
||||
|
||||
silo token create --name "CI pipeline"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Token created: CI pipeline
|
||||
API Token: silo_a1b2c3d4e5f6...
|
||||
Save this token — it will not be shown again.
|
||||
```
|
||||
|
||||
### Listing Tokens
|
||||
|
||||
```sh
|
||||
silo token list
|
||||
```
|
||||
|
||||
### Revoking a Token
|
||||
|
||||
Via the web UI settings page (click **Revoke** next to the token), or via CLI:
|
||||
|
||||
```sh
|
||||
silo token revoke <token-id>
|
||||
```
|
||||
|
||||
Revocation is immediate. Any in-flight requests using the token will fail.
|
||||
|
||||
## FreeCAD Plugin Configuration
|
||||
|
||||
The FreeCAD plugin reads the API token from two sources (checked in order):
|
||||
|
||||
1. **FreeCAD Preferences**: `Tools > Edit parameters > BaseApp/Preferences/Mod/Silo > ApiToken`
|
||||
2. **Environment variable**: `SILO_API_TOKEN`
|
||||
|
||||
To set the token in FreeCAD preferences:
|
||||
|
||||
1. Open FreeCAD
|
||||
2. Go to `Edit > Preferences > General > Macro` or use the parameter editor
|
||||
3. Navigate to `BaseApp/Preferences/Mod/Silo`
|
||||
4. Set `ApiToken` to your token string (e.g., `silo_a1b2c3d4...`)
|
||||
|
||||
Or set the environment variable before launching FreeCAD:
|
||||
|
||||
```sh
|
||||
export SILO_API_TOKEN=silo_a1b2c3d4...
|
||||
freecad
|
||||
```
|
||||
|
||||
The API URL is configured the same way via the `ApiUrl` preference or `SILO_API_URL` environment variable.
|
||||
|
||||
## Default Admin Account
|
||||
|
||||
On first deployment, configure a default admin account to bootstrap access:
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
auth:
|
||||
enabled: true
|
||||
local:
|
||||
enabled: true
|
||||
default_admin_username: "admin"
|
||||
default_admin_password: "" # Set via SILO_ADMIN_PASSWORD env var
|
||||
```
|
||||
|
||||
```sh
|
||||
export SILO_ADMIN_PASSWORD=<strong-password>
|
||||
```
|
||||
|
||||
The admin account is created on the first startup if it doesn't already exist. Subsequent startups skip creation. Change the password after first login via the database or a future admin UI.
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Minimal (Local Auth Only)
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
enabled: true
|
||||
session_secret: "" # Set via SILO_SESSION_SECRET
|
||||
|
||||
local:
|
||||
enabled: true
|
||||
default_admin_username: "admin"
|
||||
default_admin_password: "" # Set via SILO_ADMIN_PASSWORD
|
||||
```
|
||||
|
||||
### LDAP / FreeIPA
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
enabled: true
|
||||
session_secret: ""
|
||||
|
||||
local:
|
||||
enabled: true
|
||||
default_admin_username: "admin"
|
||||
default_admin_password: ""
|
||||
|
||||
ldap:
|
||||
enabled: true
|
||||
url: "ldaps://ipa.kindred.internal"
|
||||
base_dn: "dc=kindred,dc=internal"
|
||||
user_search_dn: "cn=users,cn=accounts,dc=kindred,dc=internal"
|
||||
user_attr: "uid"
|
||||
email_attr: "mail"
|
||||
display_attr: "displayName"
|
||||
group_attr: "memberOf"
|
||||
role_mapping:
|
||||
admin:
|
||||
- "cn=silo-admins,cn=groups,cn=accounts,dc=kindred,dc=internal"
|
||||
editor:
|
||||
- "cn=silo-users,cn=groups,cn=accounts,dc=kindred,dc=internal"
|
||||
- "cn=engineers,cn=groups,cn=accounts,dc=kindred,dc=internal"
|
||||
viewer:
|
||||
- "cn=silo-viewers,cn=groups,cn=accounts,dc=kindred,dc=internal"
|
||||
tls_skip_verify: false
|
||||
```
|
||||
|
||||
### OIDC / Keycloak
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
enabled: true
|
||||
session_secret: ""
|
||||
|
||||
local:
|
||||
enabled: true
|
||||
|
||||
oidc:
|
||||
enabled: true
|
||||
issuer_url: "https://keycloak.kindred.internal/realms/silo"
|
||||
client_id: "silo"
|
||||
client_secret: "" # Set via SILO_OIDC_CLIENT_SECRET
|
||||
redirect_url: "https://silo.kindred.internal/auth/callback"
|
||||
scopes: ["openid", "profile", "email"]
|
||||
admin_role: "silo-admin"
|
||||
editor_role: "silo-editor"
|
||||
default_role: "viewer"
|
||||
```
|
||||
|
||||
### CORS (Production)
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
cors:
|
||||
allowed_origins:
|
||||
- "https://silo.kindred.internal"
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Config Path |
|
||||
|----------|-------------|-------------|
|
||||
| `SILO_SESSION_SECRET` | Session encryption key | `auth.session_secret` |
|
||||
| `SILO_ADMIN_USERNAME` | Default admin username | `auth.local.default_admin_username` |
|
||||
| `SILO_ADMIN_PASSWORD` | Default admin password | `auth.local.default_admin_password` |
|
||||
| `SILO_OIDC_CLIENT_SECRET` | OIDC client secret | `auth.oidc.client_secret` |
|
||||
| `SILO_LDAP_BIND_PASSWORD` | LDAP service account password | `auth.ldap.bind_password` |
|
||||
| `SILO_API_URL` | API base URL (CLI and FreeCAD) | — |
|
||||
| `SILO_API_TOKEN` | API token (CLI and FreeCAD) | — |
|
||||
|
||||
Environment variables override config file values.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication required" on every request
|
||||
|
||||
- Verify `auth.enabled: true` in your config
|
||||
- Check that the `sessions` table exists in PostgreSQL (migration 009)
|
||||
- Ensure `SILO_SESSION_SECRET` is set (empty string is allowed for dev but not recommended)
|
||||
- Check browser cookies — `silo_session` should be present after login
|
||||
|
||||
### API token returns 401
|
||||
|
||||
- Tokens are case-sensitive. Ensure no trailing whitespace
|
||||
- Check token expiry with `silo token list`
|
||||
- Verify the user account is still active
|
||||
- Ensure the `Authorization` header format is exactly `Bearer silo_<hex>`
|
||||
|
||||
### LDAP login fails
|
||||
|
||||
- Check `ldaps://` URL is reachable from the Silo server
|
||||
- Verify `base_dn` and `user_search_dn` match your FreeIPA tree
|
||||
- Test with `ldapsearch` from the command line first
|
||||
- Set `tls_skip_verify: true` temporarily to rule out certificate issues
|
||||
- Check Silo logs for the specific LDAP error message
|
||||
|
||||
### OIDC redirect loops
|
||||
|
||||
- Verify `redirect_url` matches the Keycloak client configuration exactly
|
||||
- Check that `issuer_url` is reachable from the Silo server
|
||||
- Ensure the Keycloak client has the correct redirect URI registered
|
||||
- Check for clock skew between Silo and Keycloak servers (JWT validation is time-sensitive)
|
||||
|
||||
### Locked out (no admin account)
|
||||
|
||||
If you've lost access to all admin accounts:
|
||||
|
||||
1. Set `auth.local.default_admin_password` to a new password via `SILO_ADMIN_PASSWORD`
|
||||
2. Use a different username (e.g., `default_admin_username: "recovery-admin"`)
|
||||
3. Restart Silo — the new account will be created
|
||||
4. Log in and fix the original accounts
|
||||
|
||||
Or directly update the database:
|
||||
|
||||
```sql
|
||||
-- Reset a local user's password (generate bcrypt hash externally)
|
||||
UPDATE users SET password_hash = '<bcrypt-hash>', is_active = true WHERE username = 'admin';
|
||||
```
|
||||
|
||||
### FreeCAD plugin gets 401
|
||||
|
||||
- Verify the token is set in FreeCAD preferences or `SILO_API_TOKEN`
|
||||
- Check the API URL points to the correct server
|
||||
- Test with curl: `curl -H "Authorization: Bearer silo_..." https://silo.kindred.internal/api/items`
|
||||
Reference in New Issue
Block a user