US07-07: Automate Full Release Acceptance
This commit is contained in:
@@ -43,6 +43,9 @@ PUBLIC_PATHS = frozenset(
|
||||
{f"{API_PREFIX}/health/live", f"{API_PREFIX}/health/ready", f"{API_PREFIX}/session"}
|
||||
)
|
||||
LOOPBACK_HOSTS = frozenset({"127.0.0.1", "localhost", "::1", "[::1]"})
|
||||
# Mutating endpoints that must stay reachable while mutation itself is gated: the
|
||||
# backup a careful operator takes first, and its retention (US07-07).
|
||||
MUTATION_EXEMPT_PATHS = frozenset({f"{API_PREFIX}/backups", f"{API_PREFIX}/backups/prune"})
|
||||
|
||||
# Applied to every response. No inline script/style is used by the frontend, so the
|
||||
# policy can stay strict; `frame-ancestors 'none'` and CORP keep other pages from
|
||||
@@ -145,6 +148,7 @@ class SecurityMiddleware:
|
||||
def __init__(self, app, *, session: Session, config) -> None:
|
||||
self.app = app
|
||||
self.session = session
|
||||
self.config = config
|
||||
self.max_request_bytes = config.max_request_bytes
|
||||
self.allowed_hosts = frozenset(LOOPBACK_HOSTS | {str(config.host).lower()})
|
||||
|
||||
@@ -165,6 +169,8 @@ class SecurityMiddleware:
|
||||
allowed_hosts=self.allowed_hosts,
|
||||
max_request_bytes=self.max_request_bytes,
|
||||
)
|
||||
if refusal is None:
|
||||
refusal = self._mutation_refusal(scope)
|
||||
if refusal is not None:
|
||||
response = JSONResponse(
|
||||
status_code=refusal.status,
|
||||
@@ -183,6 +189,26 @@ class SecurityMiddleware:
|
||||
|
||||
await self.app(scope, receive, send_with_headers)
|
||||
|
||||
def _mutation_refusal(self, scope) -> Refusal | None:
|
||||
"""Refuse every mutating request while the library's dry run is unapproved.
|
||||
|
||||
One choke point for the whole API: every mutation the browser can start is a
|
||||
non-safe method under ``/api/v1``. Reading stays open — an operator has to be
|
||||
able to look at what the application found in order to approve it (US07-07).
|
||||
"""
|
||||
method = scope.get("method", "GET").upper()
|
||||
path = scope.get("path", "/")
|
||||
if method in SAFE_METHODS or not path.startswith(API_PREFIX):
|
||||
return None
|
||||
if path in MUTATION_EXEMPT_PATHS:
|
||||
return None
|
||||
from photo_pipeline.services.release import mutation_blockers
|
||||
|
||||
blockers = mutation_blockers(self.config)
|
||||
if not blockers:
|
||||
return None
|
||||
return Refusal(403, blockers[0]["code"], blockers[0]["message"])
|
||||
|
||||
|
||||
def _cookie(header: str, name: str) -> str:
|
||||
for part in header.split(";"):
|
||||
|
||||
Reference in New Issue
Block a user