Skip to content

feat(authz): segregation-of-duties conflict enforcement (#181)#193

Merged
ahliweb merged 1 commit into
mainfrom
feature/181-sod
Jul 19, 2026
Merged

feat(authz): segregation-of-duties conflict enforcement (#181)#193
ahliweb merged 1 commit into
mainfrom
feature/181-sod

Conversation

@ahliweb

@ahliweb ahliweb commented Jul 19, 2026

Copy link
Copy Markdown
Owner

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 + sodRules di module contract (v1.3.0); dikontribusi via seam komposisi Port deterministic build-time module composition untuk aplikasi ERP turunan #178. sod-rule-registry agregasi + validasi (owner==key modul, ruleKey unik, ≥2 permission konflik, enum valid); gate baru identity-access:sod-registry:check di bun run check + ci.yml (parity). Drift registry → CI RED (terbukti).
  • sql/029: sod_conflict_exceptions + sod_conflict_evaluations, tenant-scoped RLS ENABLE+FORCE, FK komposit (tenant_id,…). sql/030: 6 permission.
  • DUA titik enforcement: assignment-time (seam Implement business-scope hierarchy untuk otorisasi ERP multi-entity dan unit organisasi #180 terisi) DAN action-time fail-closed di chokepoint authorizeInTransaction (deny-overrides-allow → 403 SOD_CONFLICT) — konflik dicek saat EKSEKUSI, bukan hanya saat assignment.
  • Exception TAK PERNAH self-approved pada DUA sumbu: approver ≠ requester DAN approver ≠ subject/beneficiary. (Route create menerima subjectTenantUserId sembarang, jadi tanpa cek subject seorang beneficiary pemegang .approve bisa menyetujui bypass-nya sendiri — ditemukan oleh workflow review adversarial.) Approver dibaca-ulang dari baris DB, permission khusus, audit critical. Scope/time-bound, revocable; effective_to vs now otoritatif (revoke/expire langsung berlaku); expiry job. Aman-konkuren (CAS status='pending', diadu dua approver valid).
  • Cross-tenant: exception tenant-A tak bisa menutup tenant-B (FK komposit + FORCE RLS, dibuktikan via non-superuser awcms_app). Evaluasi bounded, non-N+1.
  • 6 operasi OpenAPI baru di fragment identity-access; snapshot beku utuh. ADR-0031 + ERD + threat model.

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 check hijau (978 test, 0 fail, build clean).
  • DB-gated vs Postgres nyata: FULL 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

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>
@ahliweb
ahliweb merged commit dd86ab6 into main Jul 19, 2026
9 checks passed
@ahliweb
ahliweb deleted the feature/181-sod branch July 19, 2026 06:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +80 to +84
const auth = await authorizeInTransaction(tx, tenantId, tokenHash, now, {
moduleKey: "identity_access",
activityCode: "business_scope_exceptions",
action: "approve"
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +179 to +180
if (matches.length === 0) {
return { blocked: false };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record clean SoD evaluations

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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement segregation of duties (SoD) dan conflict enforcement untuk transaksi ERP

1 participant