Files
ss-tools/docs/adr/ADR-0005-auth-rbac.md
2026-05-08 18:01:49 +03:00

85 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# [DEF:ADR-0005:ADR]
# @STATUS ACTIVE
# @PURPOSE Define the authentication and authorization architecture for ss-tools: local auth, optional ADFS SSO federation, RBAC model, session management, and the security boundary between ss-tools (DevOps privileges) and Superset (BI privileges).
# @RELATION DEPENDS_ON -> [ADR-0001:ADR]
# @RELATION DEPENDS_ON -> [ADR-0003:ADR]
# @RELATION CALLS -> [ADR-0004:ADR]
# @RATIONALE ss-tools manages operations that can modify production Superset instances (dashboard migration, backup, deployment). Unauthenticated or underauthorized access to these operations is a critical security risk. A dedicated RBAC system ensures that DevOps privileges (manage deployments) are cleanly separated from BI privileges (view dashboards) and that actions are auditable.
# @RATIONALE Local auth (bcrypt + JWT) is the primary path because ss-tools must work in airgapped enterprise deployments where external identity providers are unavailable. ADFS SSO is an optional federation layer for organizations that already have Active Directory.
# @RATIONALE RoleBased Access Control (RBAC) was chosen over AttributeBased Access Control (ABAC) because: (a) the system has a small, welldefined set of operations (deploy, backup, migrate, view), (b) RBAC maps naturally to organizational roles (admin, analyst, operator), (c) ABAC adds complexity without proportional benefit for this scope.
# @REJECTED Delegating all auth to Superset — rejected because ss-tools must operate when Superset is down, and Superset's auth model is designed for BI users, not DevOps operators with crossenvironment privileges.
# @REJECTED OAuth2 social login (Google, GitHub) as primary path — rejected because enterprise deployments require airgapped operation. External OAuth providers are unavailable in offline mode.
# @REJECTED Simple API key (no RBAC) — rejected because it cannot express granular permissions (admin vs analyst vs viewer), making auditability and leastprivilege impossible.
## Decision
### Authentication Flow
```
┌──────────┐ POST /api/auth/login ┌──────────────┐
│ User │ ──────────────────────────────│ FastAPI │
│ (Browser)│ {username, password} │ Backend │
│ │ ◄──────────────────────────── │ │
│ │ {access_token, refresh} │ bcrypt + │
│ │ │ JWT (HS256) │
└──────────┘ └──────┬───────┘
┌───────▼───────┐
│ PostgreSQL │
│ users table │
│ roles table │
└───────────────┘
```
### ADFS Federation (Optional)
- Enabled via `config.json: auth.adfs_enabled = true`
- SAML 2.0 flow through `python3-saml`
- ADFS users mapped to local roles via `adfs_group → ss_tools_role` mapping table
- Federated sessions still receive JWTs (stateless after initial SAML handshake)
### RBAC Model
| Role | Permissions |
|------|-------------|
| `admin` | All: manage users, manage roles, manage plugins, deploy, backup, migrate, view dashboards, view logs |
| `analyst` | View dashboards, run LLM analysis plugins, view reports, view logs (own) |
| `operator` | Deploy dashboards, run migrations, manage backups, view logs |
| `viewer` | View dashboards, view reports |
### Token Design
- **Access token**: JWT (HS256), 15minute expiry, contains `user_id`, `roles: list[str]`
- **Refresh token**: opaque random string (SHA256), 7day expiry, stored hashed in DB
- **Superset API token per environment**: AES256GCM encrypted, stored in `connection_configs` table
- **Plugin execution token**: JWT, scoped to a single `task_id`, 15minute expiry
### Security Constraints
1. Passwords: bcrypt with cost factor 12 (minimum).
2. Rate limiting: 5 failed login attempts per IP per 15 minutes → temporary IP block.
3. Token revocation: admin can revoke all sessions for a user (delete refresh tokens).
4. Audit log: all auth events (login success/failure, role change, token revoke) written to `audit_log` table.
5. Enterprise clean mode: local auth only, ADFS disabled, no external network calls for identity.
### RBAC Enforcement Pattern
```python
# backend/src/api/dependencies.py
from fastapi import Depends, HTTPException
def require_role(required_role: str):
def dependency(current_user: User = Depends(get_current_user)):
if required_role not in current_user.roles:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return current_user
return dependency
# Usage in route:
@router.post("/api/dashboards/deploy")
async def deploy(..., user: User = Depends(require_role("operator"))):
...
```
# [/DEF:ADR-0005:ADR]