feat: Phase 3 - CRUD core for Techniques, Tests and Evidence (T-014 to T-017)

- Add Pydantic schemas for Technique, Test and Evidence
- Add CRUD endpoints for Techniques (list with filters, detail, create, update, review)
- Add CRUD endpoints for Tests (create, detail, update, validate, reject)
- Add evidence upload with SHA-256 integrity and presigned download URLs
- Add MinIO/S3 storage client with bucket auto-creation on startup
- Add status_service to recalculate technique coverage from test results
- Add require_any_role RBAC dependency for multi-role authorization
- Update README with API endpoints reference and project structure
This commit is contained in:
2026-02-06 13:52:27 +01:00
parent 508f0723af
commit 4f6dd838fd
12 changed files with 958 additions and 5 deletions

57
backend/app/storage.py Normal file
View File

@@ -0,0 +1,57 @@
"""MinIO / S3-compatible object-storage helpers.
Provides thin wrappers around boto3 for bucket management, file upload
and presigned-URL generation.
"""
import boto3
from botocore.exceptions import ClientError
from app.config import settings
# ---------------------------------------------------------------------------
# Shared client (module-level singleton)
# ---------------------------------------------------------------------------
_client = boto3.client(
"s3",
endpoint_url=f"http://{settings.MINIO_ENDPOINT}",
aws_access_key_id=settings.MINIO_ACCESS_KEY,
aws_secret_access_key=settings.MINIO_SECRET_KEY,
region_name="us-east-1", # MinIO ignores this but boto3 requires it
)
# ---------------------------------------------------------------------------
# Public helpers
# ---------------------------------------------------------------------------
def ensure_bucket_exists() -> None:
"""Create the evidence bucket if it does not already exist."""
try:
_client.head_bucket(Bucket=settings.MINIO_BUCKET)
except ClientError:
_client.create_bucket(Bucket=settings.MINIO_BUCKET)
def upload_file(content: bytes, key: str) -> str:
"""Upload *content* to the evidence bucket under *key*.
Returns the key that was written (same as the input).
"""
_client.put_object(
Bucket=settings.MINIO_BUCKET,
Key=key,
Body=content,
)
return key
def get_presigned_url(key: str, expiration: int = 3600) -> str:
"""Return a presigned GET URL for *key* valid for *expiration* seconds."""
return _client.generate_presigned_url(
"get_object",
Params={"Bucket": settings.MINIO_BUCKET, "Key": key},
ExpiresIn=expiration,
)