feat(auth): implement API key authentication and management

Introduce a new API key authentication mechanism to support service-to-service
communication (e.g., Airflow, CI/CD) without browser-based JWT login.

- Add `APIKey` model and `APIKeyPrincipal` dependency for RBAC.
- Implement `require_api_key_or_jwt` dependency with environment scoping.
- Add admin CRUD endpoints for API key lifecycle management.
- Add API key management UI in System Settings.
- Update maintenance API to support explicit `environment_id` and API key auth.
- Add i18n support for API keys and connection settings.
- Include Python and Bash usage examples in the `examples/` directory.
- Update technical specifications and documentation.
This commit is contained in:
2026-05-25 11:35:27 +03:00
parent 31680a1bc9
commit 320f82ab95
39 changed files with 3359 additions and 707 deletions

View File

@@ -0,0 +1,53 @@
# #region APIKeyUtilities [C:2] [TYPE Module] [SEMANTICS auth, api_key, crypto, generation]
# @BRIEF API key generation and hashing utilities for service-to-service authentication.
# @LAYER Core
# @RELATION DEPENDS_ON -> [hashlib]
# @RELATION DEPENDS_ON -> [secrets]
# @INVARIANT generate_api_key() always returns (raw_key, prefix, key_hash) where prefix is "ssk_" + 7 chars.
# @INVARIANT hash_api_key() produces SHA-256 hex digest for lookup and storage.
import hashlib
import secrets
# #region generate_api_key [C:2] [TYPE Function]
# @BRIEF Generate a new API key in ssk_ format with SHA-256 hash.
# @POST Returns (raw_key, prefix, key_hash) — raw_key shown ONCE to caller, never stored.
# @SIDE_EFFECT Uses secrets.token_urlsafe(32) for cryptographic randomness.
# @RELATION DEPENDS_ON -> [secrets.token_urlsafe]
# @RELATION DEPENDS_ON -> [hashlib.sha256]
def generate_api_key() -> tuple[str, str, str]:
"""Generate a new API key.
Returns:
tuple[str, str, str]: (raw_key, prefix, key_hash)
- raw_key: Full key string starting with "ssk_", shown once
- prefix: First 11 characters ("ssk_" + 7 chars), stored for identification
- key_hash: SHA-256 hex digest, stored in database
"""
raw = f"ssk_{secrets.token_urlsafe(32)}" # 32 bytes -> 43 base64 chars
prefix = raw[:11] # "ssk_" + 7 chars
key_hash = hashlib.sha256(raw.encode()).hexdigest()
return raw, prefix, key_hash
# #endregion generate_api_key
# #region hash_api_key [C:1] [TYPE Function]
# @BRIEF Hash an API key string to SHA-256 hex digest for lookup.
# @PRE raw_key is a non-empty string.
# @POST Returns 64-character hex digest.
# @RELATION DEPENDS_ON -> [hashlib.sha256]
def hash_api_key(raw_key: str) -> str:
"""Hash an API key using SHA-256.
Args:
raw_key: The full API key string (e.g. ssk_...)
Returns:
str: 64-character hex digest
"""
return hashlib.sha256(raw_key.encode()).hexdigest()
# #endregion hash_api_key
# #endregion APIKeyUtilities