feat(authz): segregation-of-duties conflict enforcement (#181)#193
Conversation
Mini-first port of the SoD layer (awcms-mini #746), building on #180 and filling the `// SoD SEAM (#181)` marker. Epic #177 Wave 2. Base ships ZERO domain SoD rules — the 5+ illustrative rules live in the derived-example fixture; enforcement is inert (fail-closed) in a pure base. - SoDRuleDescriptor family + sodRules added to the module contract (v1.3.0); contributed through the #178 composition seam. sod-rule-registry aggregates + validates (owner==declaring key, unique ruleKey, >=2 conflicting keys, valid enums); new identity-access:sod-registry:check gate wired into `bun run check` + ci.yml (parity). Registry drift -> CI red (proven). - sql/029: sod_conflict_exceptions + sod_conflict_evaluations, tenant-scoped RLS ENABLE+FORCE, composite (tenant_id,...) FKs. sql/030: 6 permissions. - TWO enforcement points: assignment-time (the filled #180 seam) and ACTION-TIME fail-closed at the authorizeInTransaction chokepoint (deny-overrides-allow -> 403 SOD_CONFLICT) — a conflict is checked at execution, not only at assignment. - Exception lifecycle: NEVER self-approved on BOTH independence axes — approver != requester AND approver != subject/beneficiary (the create route accepts an arbitrary subjectTenantUserId, so without the subject check a beneficiary who holds .approve could approve their own bypass; found by the adversarial review workflow). Approver re-read from the DB row, dedicated permission, critical audit. Scope-bound + time-bound + revocable; effective_to vs now is authoritative so revoke/expire take effect immediately; expiry job included. Concurrency-safe (CAS on status='pending', raced by two valid approvers). - Cross-tenant: a tenant-A exception cannot cover tenant-B (composite FK + FORCE RLS, proven under non-superuser awcms_app). Bounded, non-N+1 evaluation. - 6 new OpenAPI ops in the identity-access fragment; frozen snapshot untouched. ADR-0031 + ERD + threat model (privilege accumulation, collusion, stale exception, self-approval incl. beneficiary-as-approver). Reviewed by an 8-dimension adversarial workflow (23 agents; each finding attacked by 3 refute-by-default skeptics). One confirmed defect (subject-as-approver self-approval) fixed + mutation-proven; the other findings refuted (notably the assignment-time TOCTOU — the action-time guard is the authoritative fail-closed gate). Verification: bun run check green (978 tests, 0 fail, build clean); FULL tests/integration/ 71 + SoD/regression — 0 fail; registry-drift, action-time, assignment-time, scope-predicate, concurrency, query-count, cross-tenant, and the beneficiary-approver guard all mutation/adversarially proven. No dependency; MFA/OIDC/business-scope/evaluateAccess not regressed. Closes #181 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 991048c47e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const auth = await authorizeInTransaction(tx, tenantId, tokenHash, now, { | ||
| moduleKey: "identity_access", | ||
| activityCode: "business_scope_exceptions", | ||
| action: "approve" | ||
| }); |
There was a problem hiding this comment.
Enforce each rule's approval permission
When a derived SoD rule sets exceptionPolicy.requiresApprovalPermission to a rule-specific permission, this endpoint still authorizes solely identity_access.business_scope_exceptions.approve, and approveSoDConflictException() is called without the rule registry or the actor's granted keys. That means any holder of the generic approve permission can approve exceptions for rules that explicitly require a stricter or different approver permission, bypassing the per-rule maker/checker policy.
Useful? React with 👍 / 👎.
| if (matches.length === 0) { | ||
| return { blocked: false }; |
There was a problem hiding this comment.
When the requested high-risk permission is covered by the SoD registry but the user does not hold the conflicting counterpart, matches is empty and this early return exits without inserting an awcms_sod_conflict_evaluations row. Because the new decision-log schema/API exposes conflictDetected=false/resolvedVia='none' and the service contract says SoD checks are appended regardless of outcome, clean allowed decisions disappear from the audit trail, so operators cannot distinguish a passing SoD check from no check running.
Useful? React with 👍 / 👎.
Ringkasan
Mini-first port lapis SoD (awcms-mini #746), di ATAS #180, mengisi seam
// SoD SEAM (#181). Epik #177 Wave 2. Base ship NOL rule SoD domain — 5+ rule ilustratif ada di fixture derived-example; enforcement inert (fail-closed) di base murni.Kontrol
SoDRuleDescriptor+sodRulesdi module contract (v1.3.0); dikontribusi via seam komposisi Port deterministic build-time module composition untuk aplikasi ERP turunan #178.sod-rule-registryagregasi + validasi (owner==key modul, ruleKey unik, ≥2 permission konflik, enum valid); gate baruidentity-access:sod-registry:checkdibun run check+ ci.yml (parity). Drift registry → CI RED (terbukti).sql/029:sod_conflict_exceptions+sod_conflict_evaluations, tenant-scoped RLSENABLE+FORCE, FK komposit(tenant_id,…).sql/030: 6 permission.authorizeInTransaction(deny-overrides-allow → 403SOD_CONFLICT) — konflik dicek saat EKSEKUSI, bukan hanya saat assignment.subjectTenantUserIdsembarang, jadi tanpa cek subject seorang beneficiary pemegang.approvebisa menyetujui bypass-nya sendiri — ditemukan oleh workflow review adversarial.) Approver dibaca-ulang dari baris DB, permission khusus, audit critical. Scope/time-bound, revocable;effective_tovs now otoritatif (revoke/expire langsung berlaku); expiry job. Aman-konkuren (CASstatus='pending', diadu dua approver valid).awcms_app). Evaluasi bounded, non-N+1.Review — adversarial workflow (bukan single-pass)
Direview oleh workflow 8-dimensi (23 agent): tiap temuan diserang 3 skeptik refute-by-default. Satu defect terkonfirmasi (subject-as-approver self-approval) → diperbaiki + mutation-proven; temuan lain terrefutasi (a.l. TOCTOU assignment-time — guard action-time-lah gerbang fail-closed otoritatif).
Verifikasi
bun run checkhijau (978 test, 0 fail, build clean).tests/integration/71 + SoD/regresi — 0 fail. Registry-drift, action-time, assignment-time, scope-predicate, concurrency, query-count, cross-tenant, dan guard beneficiary-approver semua mutation/adversarially proven RED. Tanpa dependency; MFA/OIDC/business-scope/evaluateAccess tak diregresi.Closes #181
🤖 Generated with Claude Code