feat(V1-API): implement TREX-Basic consumer API contract with encryption and persistence
- Add V1 API endpoints: assets, ledger, user-info - Implement Fernet-based credential payload encryption (req-010) - Extend health response schema with runtime info (req-004) - Add service token runtime ID issuance (req-005) - Implement database schema migration for V1 API (req-009) - Add comprehensive test coverage for V1 endpoints - Update SRS with 10 new requirements (REQ-004 through REQ-011) - Create batch planning, design, and verification for BAT-001 - Integrate review-discovery StagePilot skill - Update documentation indices and baseline specs
This commit is contained in:
@@ -28,6 +28,7 @@ class AuthContext:
|
||||
token_id: str | None = None
|
||||
token_type: str | None = None
|
||||
token_source: str = "env"
|
||||
runtime_id: str | None = None
|
||||
|
||||
def get_auth_repository(settings: Settings = Depends(get_settings)) -> Generator[PortfolioRepository, None, None]:
|
||||
repository = get_portfolio_repository_from_settings(settings)
|
||||
@@ -66,6 +67,7 @@ def _resolve_auth_context(token: str, settings: Settings, repository: PortfolioR
|
||||
token_id=principal.token_id,
|
||||
token_type=principal.token_type,
|
||||
token_source="db",
|
||||
runtime_id=principal.runtime_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+17
-1
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field, computed_field
|
||||
from pydantic import Field, computed_field, model_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
# Load .env file from project root
|
||||
@@ -50,6 +50,7 @@ class Settings(BaseSettings):
|
||||
loki_host: str = Field(default="localhost", validation_alias="LOKI_HOST")
|
||||
loki_port: int = Field(default=3100, validation_alias="LOKI_PORT")
|
||||
loki_enabled: bool = Field(default=False, validation_alias="LOKI_ENABLED")
|
||||
credential_encryption_key: str = Field(default="", validation_alias="CREDENTIAL_ENCRYPTION_KEY")
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="PORTFOLIOHUB_",
|
||||
@@ -58,6 +59,21 @@ class Settings(BaseSettings):
|
||||
populate_by_name=True,
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_credential_encryption_key(self) -> "Settings":
|
||||
# Only validate at runtime when the key is actually used.
|
||||
# This avoids blocking test environments that don't exercise encryption.
|
||||
return self
|
||||
|
||||
def require_credential_encryption_key(self) -> str:
|
||||
"""Return the key or raise RuntimeError if it is not configured."""
|
||||
if not self.credential_encryption_key.strip():
|
||||
raise RuntimeError(
|
||||
"CREDENTIAL_ENCRYPTION_KEY is not configured. "
|
||||
"Generate one with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
||||
)
|
||||
return self.credential_encryption_key
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def allowed_origins(self) -> list[str]:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Fernet symmetric encryption utilities for credential payloads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken # noqa: F401 — re-exported for callers
|
||||
|
||||
|
||||
def make_fernet(key: str) -> Fernet:
|
||||
"""Return a Fernet instance from a base64-encoded key string."""
|
||||
return Fernet(key.encode() if isinstance(key, str) else key)
|
||||
|
||||
|
||||
def encrypt_payload(key: str, plaintext: str) -> str:
|
||||
"""Encrypt *plaintext* and return the base64 ciphertext string."""
|
||||
f = make_fernet(key)
|
||||
return f.encrypt(plaintext.encode()).decode()
|
||||
|
||||
|
||||
def decrypt_payload(key: str, ciphertext: str) -> str:
|
||||
"""Decrypt *ciphertext* and return the plaintext string.
|
||||
|
||||
Raises ``cryptography.fernet.InvalidToken`` if the key is wrong or the
|
||||
token is malformed.
|
||||
"""
|
||||
f = make_fernet(key)
|
||||
return f.decrypt(ciphertext.encode()).decode()
|
||||
Reference in New Issue
Block a user