mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-19 09:46:33 +00:00
fix(ndo): tenant-delete guard also scans tenant policy templates (F5b) (v0.26.0)
The in-use guard added in 0.22.0 (F5) only scanned SCHEMA templates'
tenantId, so a tenant referenced only by a tenant policy template
(tenant_policy_templates[*].tenantPolicyTemplate.template.tenantId — the
same field path _get_template_objects/_backfill_policy_uuids already
traverse) could be deleted, leaving a dangling reference. Real NDO refuses
to delete an in-use tenant regardless of which template type holds the
reference.
DELETE /mso/api/v1/tenants/{id} (and the bare /api/v1 twin) now scans
both. A policy-template-only reference returns 400 "Tenant '<id>' is
referenced by tenant policy template(s): <names> — delete those templates
first"; the wording is a format-mirrored approximation of the
hardware-grounded schema-template message (no real-NDO capture of this
specific rejection text exists yet — the code comment says so). The
schema-template message is byte-for-byte unchanged. Deleting the
referencing template still releases the guard. Malformed policy-template
entries default-allow (fail-safe, consistent with the sim's philosophy).
Verification: full pytest 1147 passed (1143 baseline + 4 new TDD tests,
red-phase confirmed pre-fix; all 8 pre-existing F5 tests intact);
independent opus review APPROVE (over-rejection trace: different-tenant
policy template still deletes clean; release-test proven non-vacuous);
live golden gate on restarted sim — create tenant -> create referencing
policy template -> DELETE 400 golden -> delete template -> DELETE 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zdUTqU9fvCCvF3uVGZsu1
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c2959ab2cb
commit
4ca201085b
+49
-8
@@ -180,6 +180,21 @@ def make_ndo_app(state: NdoState, apic_states: dict[str, Any] | None = None) ->
|
||||
# GET /mso/api/v1/tenants ↔ GET /api/v1/tenants dual-shape pair above
|
||||
# (`delegate_to: localhost` cisco.mso tasks skip the /mso prefix —
|
||||
# see the PR-13 templates-store comment below).
|
||||
#
|
||||
# F5b: the F5 guard above only ever scanned SCHEMA templates — a
|
||||
# tenant referenced ONLY by a tenant policy template
|
||||
# (`tenant_policy_templates[*].tenantPolicyTemplate.template.
|
||||
# tenantId` — same field path `_get_template_objects`/
|
||||
# `_backfill_policy_uuids` already traverse above) deleted cleanly,
|
||||
# fails-open vs. real NDO which refuses regardless of which template
|
||||
# type holds the reference. Extend the scan to cover both. The
|
||||
# policy-template branch's exact wording is an approximation mirrored
|
||||
# from the F5 schema-template message's format/shape — no real-NDO
|
||||
# hardware capture of THIS specific rejection text exists yet, unlike
|
||||
# the schema-template wording above (see F5's commit evidence). A
|
||||
# malformed policy-template entry (non-dict `tenantPolicyTemplate`/
|
||||
# `template`) default-allows rather than crashing the guard, matching
|
||||
# this sim's fail-safe philosophy elsewhere.
|
||||
@app.delete("/mso/api/v1/tenants/{tenant_id}")
|
||||
@app.delete("/api/v1/tenants/{tenant_id}")
|
||||
async def delete_tenant(tenant_id: str):
|
||||
@@ -188,7 +203,7 @@ def make_ndo_app(state: NdoState, apic_states: dict[str, Any] | None = None) ->
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Tenant '{tenant_id}' not found"
|
||||
)
|
||||
referencing = sorted(
|
||||
referencing_schemas = sorted(
|
||||
{
|
||||
detail.get("displayName") or detail.get("name") or detail["id"]
|
||||
for detail in state.schema_details.values()
|
||||
@@ -196,14 +211,40 @@ def make_ndo_app(state: NdoState, apic_states: dict[str, Any] | None = None) ->
|
||||
if isinstance(tmpl, dict) and tmpl.get("tenantId") == tenant_id
|
||||
}
|
||||
)
|
||||
if referencing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
f"Tenant '{tenant_id}' is referenced by schema(s): "
|
||||
f"{', '.join(referencing)} — delete those schemas first"
|
||||
),
|
||||
referencing_policy_templates = sorted(
|
||||
{
|
||||
doc.get("displayName") or key
|
||||
for key, doc in state.tenant_policy_templates.items()
|
||||
if isinstance(doc, dict)
|
||||
and isinstance(doc.get("tenantPolicyTemplate"), dict)
|
||||
and isinstance(
|
||||
doc["tenantPolicyTemplate"].get("template"), dict
|
||||
)
|
||||
and doc["tenantPolicyTemplate"]["template"].get("tenantId")
|
||||
== tenant_id
|
||||
}
|
||||
)
|
||||
if referencing_schemas and referencing_policy_templates:
|
||||
detail_msg = (
|
||||
f"Tenant '{tenant_id}' is referenced by schema(s): "
|
||||
f"{', '.join(referencing_schemas)} — delete those schemas "
|
||||
f"first; and tenant policy template(s): "
|
||||
f"{', '.join(referencing_policy_templates)} — delete those "
|
||||
f"templates first"
|
||||
)
|
||||
elif referencing_schemas:
|
||||
detail_msg = (
|
||||
f"Tenant '{tenant_id}' is referenced by schema(s): "
|
||||
f"{', '.join(referencing_schemas)} — delete those schemas first"
|
||||
)
|
||||
elif referencing_policy_templates:
|
||||
detail_msg = (
|
||||
f"Tenant '{tenant_id}' is referenced by tenant policy "
|
||||
f"template(s): {', '.join(referencing_policy_templates)} — "
|
||||
f"delete those templates first"
|
||||
)
|
||||
if referencing_schemas or referencing_policy_templates:
|
||||
raise HTTPException(status_code=400, detail=detail_msg)
|
||||
state.tenants.remove(tenant)
|
||||
return {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user