Core changes: - Add @defgroup/@ingroup to 1791 C2+ contracts (555 files) for HCA 128× pre-training DSA grouping - Add §0.1 Pre-Training Frequency matrix to semantics-core - Add §VIII Attention Architecture rules (ATTN_1-4) with MLA/CSA/HCA/DSA mechanics - Add @defgroup/@ingroup to canonical syntax (§II) and all contract examples Agent prompts (5 files): - Add ZERO-STATE RATIONALE with MLA/CSA/HCA/DSA compression mechanics - Add pre-training note: @RATIONALE/@REJECTED are in-context learned tags - svelte-coder: add missing #region contract, fix Svelte rule violations - python-coder/fullstack-coder: honor function contracts from speckit plan - qa-tester: add attention compliance audit (P3 ATTN_1-4 checks) Skills (6 files): - Translate all axiom_config descriptions to English - Fix doc_dirs to index .opencode/ and .specify/ - Deduplicate 5× complexity_rules → single global_tags catalog - Reduce semantics-svelte 591→485 lines (remove duplicate code blocks) - Fix semantics-testing: 'Short IDs' → 'Short hierarchical IDs' - Fix all examples: flat IDs → hierarchical Domain.Name format - Fix Svelte examples: replace raw Tailwind + <button> with semantic tokens + /ui Speckit workflow (commands + templates): - speckit.plan: add Function-Level Contracts for C3+ with @PRE/@POST/@TEST_EDGE - speckit.plan: add Attention Compliance Gate (ATTN_1-4 before contract generation) - speckit.tasks: add function contract inlining format (constraints in task description) - speckit.specify: load semantics-core for spec density rules - spec-template: add #region contract, @SEMANTICS grouping, hierarchical IDs - ux-reference-template: add #region wrapper - plan-template: add attention gate, @defgroup/@ingroup guidance - tasks-template: add attention audit + rebuild + orphan check tasks - constitution.md: translate to English, add Principle VIII (attention-optimized contracts) Reference modules rewritten (hierarchical IDs + full contracts): - Auth.Jwt: 6 child contracts with @RATIONALE/@REJECTED/@TEST_EDGE - Api.Auth: 5 endpoints with @TEST_EDGE + molecular CoT markers - Migration.Model: @defgroup Migration with 18 @ACTION + 6 @INVARIANT Scripts: - add_defgroup_ingroup.py: zero-risk additive @ingroup migration (1791 insertions) - migrate_hierarchical.py: flat→hierarchical ID dry-run analysis (792 contracts) - merge_prompts.py: merge all prompts/skills/commands into one review file Config: - axiom_config.yaml: 749→395 lines (-47%), English, doc_dirs include prompts - Fix test_datasets.py import collision (rename → test_datasets_routes.py) - Fix test_preview.py: SupersetClient→get_superset_client, AsyncMock, logger f-string
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
# #region AdminApiKeyRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, admin, api_key, crud]
|
|
# @defgroup Api Module group.
|
|
# @BRIEF Admin API endpoints for API key management — list, generate (one-time reveal), and revoke.
|
|
# @LAYER API
|
|
# @RELATION DEPENDS_ON -> [APIKeyModel]
|
|
# @RELATION DEPENDS_ON -> [APIKeyUtilities]
|
|
# @RELATION DEPENDS_ON -> [EXT:code:has_permission_admin_settings_WRITE]
|
|
# @INVARIANT GET /api/admin/api-keys NEVER returns key_hash or raw_key.
|
|
# @INVARIANT POST /api/admin/api-keys returns raw_key ONCE — never stored, never retrievable again.
|
|
# @INVARIANT DELETE /api/admin/api-keys/{id} soft-deletes (active=False), preserves row for audit.
|
|
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ...core.auth.api_key import generate_api_key
|
|
from ...core.database import get_db
|
|
from ...dependencies import has_permission
|
|
from ...models.api_key import APIKey
|
|
|
|
# #region router [TYPE Variable]
|
|
# @ingroup Api
|
|
# @BRIEF APIRouter for admin API key management routes.
|
|
router = APIRouter(prefix="/api/admin/api-keys", tags=["admin", "api-keys"])
|
|
# #endregion router
|
|
|
|
|
|
# ── Pydantic schemas ──────────────────────────────────────────
|
|
|
|
# #region ApiKeyCreateRequest [C:1] [TYPE Class]
|
|
class ApiKeyCreateRequest(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
environment_id: str | None = None
|
|
permissions: list[str] = Field(..., min_length=1)
|
|
expires_at: datetime | None = None
|
|
# #endregion ApiKeyCreateRequest
|
|
|
|
|
|
# #region ApiKeyCreateResponse [C:1] [TYPE Class]
|
|
class ApiKeyCreateResponse(BaseModel):
|
|
id: str
|
|
raw_key: str
|
|
prefix: str
|
|
name: str
|
|
environment_id: str | None
|
|
permissions: list[str]
|
|
active: bool
|
|
created_at: datetime
|
|
expires_at: datetime | None
|
|
# #endregion ApiKeyCreateResponse
|
|
|
|
|
|
# #region ApiKeyListItem [C:1] [TYPE Class]
|
|
class ApiKeyListItem(BaseModel):
|
|
id: str
|
|
name: str
|
|
prefix: str
|
|
environment_id: str | None
|
|
permissions: list[str]
|
|
active: bool
|
|
created_at: datetime
|
|
expires_at: datetime | None
|
|
last_used_at: datetime | None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
# #endregion ApiKeyListItem
|
|
|
|
|
|
# #region ApiKeyRevokeResponse [C:1] [TYPE Class]
|
|
class ApiKeyRevokeResponse(BaseModel):
|
|
id: str
|
|
status: str
|
|
# #endregion ApiKeyRevokeResponse
|
|
|
|
|
|
# ── Routes ────────────────────────────────────────────────────
|
|
|
|
# #region list_api_keys [C:2] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF List all API keys — NEVER returns key_hash or raw_key.
|
|
# @PRE Requires admin:settings WRITE permission.
|
|
# @POST Returns list of ApiKeyListItem without sensitive fields.
|
|
@router.get("/", response_model=list[ApiKeyListItem])
|
|
async def list_api_keys(
|
|
db: Session = Depends(get_db),
|
|
_=Depends(has_permission("admin:settings", "WRITE")),
|
|
):
|
|
keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()
|
|
return [
|
|
ApiKeyListItem(
|
|
id=k.id,
|
|
name=k.name,
|
|
prefix=k.prefix,
|
|
environment_id=k.environment_id,
|
|
permissions=list(k.permissions or []),
|
|
active=k.active,
|
|
created_at=k.created_at,
|
|
expires_at=k.expires_at,
|
|
last_used_at=k.last_used_at,
|
|
)
|
|
for k in keys
|
|
]
|
|
# #endregion list_api_keys
|
|
|
|
|
|
# #region create_api_key [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Generate a new API key — returns raw key ONCE, never stored or retrievable again.
|
|
# @PRE Requires admin:settings WRITE permission. name is required, at least one permission.
|
|
# @POST Creates APIKey row with SHA-256 hash. Returns raw key in response.
|
|
# @SIDE_EFFECT Generates cryptographically random key, stores hash in DB.
|
|
# @RELATION DEPENDS_ON -> [generate_api_key]
|
|
@router.post("/", response_model=ApiKeyCreateResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_api_key(
|
|
request: ApiKeyCreateRequest,
|
|
db: Session = Depends(get_db),
|
|
_=Depends(has_permission("admin:settings", "WRITE")),
|
|
):
|
|
# Validate
|
|
if not request.name.strip():
|
|
raise HTTPException(status_code=400, detail="Name is required")
|
|
if not request.permissions:
|
|
raise HTTPException(status_code=400, detail="At least one permission is required")
|
|
|
|
# Generate key
|
|
raw_key, prefix, key_hash = generate_api_key()
|
|
|
|
# Store hash only
|
|
api_key = APIKey(
|
|
key_hash=key_hash,
|
|
prefix=prefix,
|
|
name=request.name.strip(),
|
|
environment_id=request.environment_id,
|
|
permissions=request.permissions,
|
|
active=True,
|
|
expires_at=request.expires_at,
|
|
)
|
|
db.add(api_key)
|
|
db.commit()
|
|
db.refresh(api_key)
|
|
|
|
return ApiKeyCreateResponse(
|
|
id=api_key.id,
|
|
raw_key=raw_key,
|
|
prefix=api_key.prefix,
|
|
name=api_key.name,
|
|
environment_id=api_key.environment_id,
|
|
permissions=list(api_key.permissions or []),
|
|
active=api_key.active,
|
|
created_at=api_key.created_at,
|
|
expires_at=api_key.expires_at,
|
|
)
|
|
# #endregion create_api_key
|
|
|
|
|
|
# #region revoke_api_key [C:2] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Revoke an API key by setting active=False. Preserves row for audit.
|
|
# @PRE Requires admin:settings WRITE permission.
|
|
# @POST Sets active=False on the key. Returns 404 if already revoked or not found.
|
|
@router.delete("/{key_id}", response_model=ApiKeyRevokeResponse)
|
|
async def revoke_api_key(
|
|
key_id: str,
|
|
db: Session = Depends(get_db),
|
|
_=Depends(has_permission("admin:settings", "WRITE")),
|
|
):
|
|
api_key = db.query(APIKey).filter(APIKey.id == key_id).first()
|
|
if not api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="API key not found",
|
|
)
|
|
if not api_key.active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="API key is already revoked",
|
|
)
|
|
|
|
api_key.active = False
|
|
db.commit()
|
|
|
|
return ApiKeyRevokeResponse(
|
|
id=api_key.id,
|
|
status="revoked",
|
|
)
|
|
# #endregion revoke_api_key
|
|
|
|
# #endregion AdminApiKeyRoutes
|