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,36 @@
# #region APIKeyModel [C:1] [TYPE Module] [SEMANTICS sqlalchemy, api_key, auth, model]
# @BRIEF SQLAlchemy model for API Key authentication — stores SHA-256 hash only, raw key shown once at creation.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [MappingModels]
# @INVARIANT key_hash is stored as SHA-256 hex digest (64 chars). prefix stores "ssk_" + first 7 chars.
# @INVARIANT Raw key is NEVER stored — shown once at creation and discarded.
import uuid
from datetime import datetime, timezone
from sqlalchemy import Boolean, Column, DateTime, JSON, String
from .mapping import Base
# #region APIKey [C:1] [TYPE Class]
# @BRIEF Stores API key metadata and SHA-256 hash for service-to-service authentication.
class APIKey(Base):
__tablename__ = "api_keys"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
key_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA-256 hex digest
prefix = Column(String(11), nullable=False) # "ssk_" + 7 chars
name = Column(String(255), nullable=False) # Human-readable label
environment_id = Column(String(50), nullable=True) # Scoped to environment; null = all
permissions = Column(JSON, nullable=False) # e.g. ["maintenance:start", "maintenance:end"]
active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
expires_at = Column(DateTime, nullable=True)
last_used_at = Column(DateTime, nullable=True)
def __repr__(self) -> str:
return f"<APIKey {self.prefix}... ({self.name})>"
# #endregion APIKey
# #endregion APIKeyModel