Finish TEN-WP-0010-T03/T04: audited grouping mutation
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m5s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m5s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e11b6a155
commit
b998ca2332
6 changed files with 422 additions and 3 deletions
|
|
@ -114,6 +114,15 @@ class LifecycleRequest(BaseModel):
|
|||
correlation_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class SetGroupingRequest(BaseModel):
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
grouping: str = Field(min_length=1)
|
||||
actor: str
|
||||
reason: str = Field(min_length=1)
|
||||
correlation_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class GuardrailLimitBody(BaseModel):
|
||||
"""A ceiling, as a consumer states it.
|
||||
|
||||
|
|
@ -385,6 +394,35 @@ def create_app(
|
|||
mutate=lambda tenant, at: tenant.reactivate(at=at),
|
||||
)
|
||||
|
||||
# -- Reclassification (TEN-WP-0010) -----------------------------------
|
||||
# Separate from PATCH /tenants/{id} on purpose: grouping resolves spend
|
||||
# ceilings, so policy must be able to permit a rename without permitting a
|
||||
# reclassification, and the audit trail must show which one happened.
|
||||
|
||||
@app.post("/tenants/{tenant_id}/grouping")
|
||||
async def set_grouping(
|
||||
tenant_id: str,
|
||||
payload: SetGroupingRequest,
|
||||
response: Response,
|
||||
idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
if_match: str | None = Header(default=None, alias="If-Match"),
|
||||
) -> dict:
|
||||
return _lifecycle_mutation(
|
||||
store=store,
|
||||
authorizer=authorizer,
|
||||
action="tenant.grouping.set",
|
||||
tenant_id=tenant_id,
|
||||
actor=payload.actor,
|
||||
reason=payload.reason,
|
||||
correlation_id=payload.correlation_id,
|
||||
idempotency_key=idempotency_key,
|
||||
if_match=if_match,
|
||||
response=response,
|
||||
event_type="tenant_grouping_changed",
|
||||
extra_fingerprint={"grouping": payload.grouping},
|
||||
mutate=lambda tenant, at: tenant.with_grouping(payload.grouping, at=at),
|
||||
)
|
||||
|
||||
# -- Guardrail API (TEN-WP-0006) --------------------------------------
|
||||
# Reading a ceiling and changing one are distinct actions, so policy can
|
||||
# give flex-auth the read without giving anything the write.
|
||||
|
|
@ -700,6 +738,10 @@ def _lifecycle_mutation(
|
|||
) from exc
|
||||
except (InvalidLifecycleTransitionError, TenantRetiredError) as exc:
|
||||
raise LifecycleError(409, "invalid_lifecycle_transition", str(exc), correlation_id) from exc
|
||||
except InvalidTenantIdentifierError as exc:
|
||||
# TEN-WP-0010: an unknown grouping. Distinct from invalid_update so a
|
||||
# caller can tell "not a grouping" from "nothing changed".
|
||||
raise LifecycleError(400, "invalid_grouping", str(exc), correlation_id) from exc
|
||||
except (ImmutableFieldError, EmptyUpdateError) as exc:
|
||||
raise LifecycleError(400, "invalid_update", str(exc), correlation_id) from exc
|
||||
except StoreUnavailableError as exc:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue