- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files - Rename git bundle file ss-tools.bundle → superset-tools.bundle - Update .gitignore pattern accordingly - Preserve variable names (hasSsTools etc.) and code identifiers
4.8 KiB
4.8 KiB
APIKey Entity (NEW — 2026-05-25)
Implementation: backend/src/models/api_key.py
Database table: api_keys
Base: backend/src/models/mapping.py (Base)
Service-to-service authentication token for external tools. Provides bearer-token auth to maintenance endpoints as an alternative to browser-based JWT.
┌─────────────────────────────────────────┐
│ api_keys │
│ (one key per external tool) │
│ ──────────────────────────────────── │
│ id: String (UUID PK) │
│ key_hash: String(64) (SHA-256 hex) │
│ prefix: String(11) ("ssk_" + 7 chars) │
│ name: String(255) │
│ environment_id: String(50) (nullable) │
│ permissions: JSON │
│ active: Boolean │
│ created_at: DateTime │
│ expires_at: DateTime (nullable) │
│ last_used_at: DateTime (nullable) │
└─────────────────────────────────────────┘
Note: id is stored as String containing a UUID, not as a PostgreSQL UUID type (SQLite-compatible).
Columns
| Column | Type | Description |
|---|---|---|
id |
UUID, PK |
Unique identifier |
key_hash |
VARCHAR(64), NOT NULL, UNIQUE |
SHA-256 hash of the raw key. Raw key NEVER stored. |
prefix |
VARCHAR(11), NOT NULL |
First 11 chars of raw key ("ssk_" + 7 chars). Displayed in admin list for identification. Hashed alone: attacker with prefix only cannot reconstruct key. |
name |
VARCHAR(255), NOT NULL |
Human-readable label: "Airflow ETL Production", "GitLab CI/CD Staging" |
environment_id |
VARCHAR(50), NULLABLE |
Bound Superset environment. NULL or "*" = all environments (admin-only). If set, request body environment_id must match or be omitted (defaults to this). |
permissions |
JSON, NOT NULL |
Fine-grained ops: ["maintenance:start", "maintenance:end", "maintenance:end_all"]. Any subset. |
active |
BOOLEAN, DEFAULT TRUE |
Revocation flag. false = rejected with 401. |
created_at |
DATETIME, DEFAULT NOW() |
|
expires_at |
DATETIME, NULLABLE |
Auto-expiry. Past date = rejected with 401. |
last_used_at |
DATETIME, NULLABLE |
Updated on each authenticated request. Audit trail. |
Invariants
- Raw key never stored — only
key_hash=SHA256(ssk_<token>). Verification: hash incoming header, compare to stored hash. - Prefix displayed, not hashed — allows admin to identify keys in list (
"ssk_M7xqaP...") without exposing the full key. - No refresh — if compromised, admin revokes (
active=false) and generates new key viaPOST /api/admin/api-keys. - Environment gate — key's
environment_idgates Superset target. Key forss-devcannot start maintenance onss-prod. - Read-once secret —
POST /api/admin/api-keysreturns{id, raw_key, prefix, name, ...}. Theraw_keyfield is the ONLY output. SubsequentGETcalls never include it.
Key Format
ssk_<43 random URL-safe chars>
Example: ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3
- Prefix
ssk_identifies the token as an superset-tools server key (vs Superset tokens, API keys for other services). - 43 random chars from
secrets.token_urlsafe(32)= 192 bits entropy → ~2^192 keyspace. - Format is compatible with env vars, curl headers, CI/CD secret variables.
Verification Flow (in dependency)
async def get_api_key_principal(
api_key: str = Header(None, alias="X-API-Key"),
db: Session = Depends(get_db),
):
if not api_key:
return None # No API key header; fall through to JWT
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
key_record = db.query(APIKey).filter(
APIKey.key_hash == key_hash,
APIKey.active == True,
).first()
if not key_record:
raise HTTPException(401, "Invalid or revoked API key")
if key_record.expires_at and key_record.expires_at < datetime.now(timezone.utc):
raise HTTPException(401, "API key has expired")
# Update last_used_at (non-blocking)
key_record.last_used_at = datetime.now(timezone.utc)
db.flush()
return APIKeyPrincipal(key_record)
Security Notes
- Hash only — matches CWE-312 compliance. Raw key is ephemeral.
- No key rotation — deliberate. Rotation = revoke + regenerate. Simpler, safer.
- Prefix hashing — 11 chars visible. Attacker with prefix needs to brute-force remaining 37 characters (192 bits remaining).
- No rate limiting (v1) — admin responsibility. Future: cross-cutting rate limiter on
X-API-Keyheader.