Skip to content

Latest commit

 

History

History
175 lines (149 loc) · 10.8 KB

File metadata and controls

175 lines (149 loc) · 10.8 KB

W4 Audit — §4-22 (add-anything) · §4-23 (window model / drag)

Date: 2026-07-10/11 Scope: docs/design/oyatie-console/DESIGN.md §4-22 and §4-23 directives. Method: live browser exercise (Playwright against Chromium) of a freshly migrated, real backend + real Postgres dev stack — no mocks, no stubs. No code changes made; this is a findings register only.

Environment

  • Fresh worktree .worktrees/w4-audits off origin/main (7aa4b990).
  • Scratch database created via psql inside the already-running console-dev Postgres container (console_scratch_w4audit, port 55432) — did not touch any other lane's console_dev database or the crash-looping console-dev-app-1 container (pre-existing, unrelated breakage — CONSOLE_JWT_PUBLIC_KEY_PEM config error in that container, left untouched).
  • Backend: node scripts/dev-up.mjs bootstrap with CONSOLE_DEV_AUTH_E2E=1, CONSOLE_POSTGRES_DB=console_scratch_w4audit, CONSOLE_DEV_HTTP_PORT=8190 — real console-app --features dev-auth, 162/162 migrations applied, /readyz green.
  • Frontend: vite on :5173, real dev-auth role-switch login (SUPER_ADMIN, org 00000000-0000-0000-0000-0000000000a1 / KNL), real WebAuthn passkey enrollment via a CDP virtual authenticator (not skipped/mocked) when the onboarding gate required it.
  • Driven with Playwright (already a project devDependency) since no chrome-devtools MCP was available in this subagent's toolset; screenshots saved to the session scratchpad.

Finding 1 — CRITICAL — /policy/authorize/bulk (and /policy/authorize) is wired to a 5-action allowlist, so every fine-grained PolicyGated affordance console-wide silently disappears for every role, including SUPER_ADMIN

Where: backend/crates/platform/authz-rest/src/lib.rs (authorize_bulk, authorize handlers) → both call simulate_policies = console_platform_authz::cedar_pbac::authoring::simulate (backend/crates/platform/authz/src/cedar_pbac/authoring.rs:594).

Root cause: authoring::simulate_inner hard-denies any request whose action is not in the compile-time constant AUTHORING_ACTIONS (authoring.rs:191):

const AUTHORING_ACTIONS: &[&str] = &[
    "view", "edit", "read_field", "console:configure", "console:deploy",
];

This is the no-code policy-authoring canvas's own tiny action vocabulary (for /policy/simulate what-if runs), not a general-purpose runtime authorization action registry. But the frontend's real, live authorization gate — BulkPolicyGateProvider in web/src/console/policy/PolicyGated.tsx — calls POST /api/v1/policy/authorize/bulk with the console's actual granular action vocabulary: ontology.schema.create, ontology.schema.edit, ontology.instance.open, console.object_explorer.node.create, evidence.custody.manage, governance.approval.decide, console.workflows.workflow.publish.approve, etc. — over 100 distinct action strings across web/src/console/*/types.ts, essentially none of which are in the 5-item AUTHORING_ACTIONS list.

Every one of those requests comes back effect: "deny", reason: "action \"…\" is not authorizable; deny-by-omission" — for every subject, every org, every role, because the deny happens before any policy/role lookup even runs. PolicyGated renders nothing on deny (web/src/console/policy/PolicyGated.tsx:154, "deny-by-omission render gate… absent, not disabled"), so the affected UI doesn't 403 or grey out — it simply never appears, with no error surfaced to the user.

Repro:

  1. Boot the stack as above, log in as SUPER_ADMIN (dev-auth role switch).
  2. Open browser devtools / a request interceptor, navigate to /ontology.
  3. Observe the real app-issued POST /api/v1/policy/authorize/bulk request; response body:
    {"decisions":[
      {"effect":"deny","reason":"action \"ontology.schema.create\" is not authorizable; deny-by-omission", ...},
      {"effect":"deny","reason":"action \"ontology.schema.approve\" is not authorizable; deny-by-omission", ...},
      ...
    ]}
  4. On screen: the "타입" (Type) rail shows 0개 and the entire inline add-type form (새 타입 이름 input + 타입 추가 button, OntologyManagerScreen.tsx:308-320, gated by <PolicyGated action={ONTOLOGY_MANAGER_ACTIONS.typeCreate}>) is absent from the DOM — not disabled, not present at all — for the platform's own SUPER_ADMIN.

Impact / severity: Critical, blast radius is essentially the whole console. This is not scratch-DB-specific and not a dev-auth artifact — the 5-action allowlist is a Rust compile-time constant on main (1c361252, PR #432), so it is present in the production build too and no amount of policy/data seeding can work around it. It is the direct blocker for §4-22 (the no-code add-anything promise is unreachable at its very first step for the type registry, and by construction for every other PolicyGated-wrapped create affordance that uses the console's real action vocabulary rather than the authoring canvas's view/edit/read_field verbs).

Suggested fix direction (no code changed here, out of this audit's lane): either (a) give authorize/authorize_bulk a proper runtime action registry independent of AUTHORING_ACTIONS (the latter should stay scoped to /policy/simulate and the no-code canvas), or (b) if Cedar activation for the wider console is intentionally still dark/unimplemented, gate PolicyGated on a feature flag so it doesn't silently blank out the entire UI while Cedar enforcement is mid-rollout. File as its own charter — this is bigger than a §4-22/23 UI polish item.

§4-22 (add-anything) — step-by-step score

Attempted the full flow: register type via manager → codes drag/parse → module surface renders → create instance → lifecycle.

Step Result Note
1. Register a new type via Ontology Manager BROKEN Blocked entirely by Finding 1 — the "새 타입 이름" input and "타입 추가" button never render for any role.
2. Codes drag/parse into the new type UNTESTABLE No type can be created to test this on.
3. Module surface renders the new type UNTESTABLE Same.
4. Create an instance of the new type UNTESTABLE Same — also separately gated by ontology.instance.* actions, which are equally outside AUTHORING_ACTIONS, so this step would be blocked a second, independent way even if step 1 were fixed by data alone.
5. Lifecycle transition on the instance UNTESTABLE Same.

§4-22's core promise ("사용자가 마주치는 모든 UI 요소는 그 자리에서 새 항목을 추가할 수 있어야 한다") is not met for the flagship case named in the directive (온톨로지 타입 저작) in the current build. Every other add-affordance gated the same way (table new-column config mode, enum-chip new-value proposal, stat-bar new-stat, saved filter presets — all listed in §4-22) should be assumed similarly at-risk wherever they route through PolicyGated with a non-AUTHORING_ACTIONS action string; this audit did not exhaustively re-test each one individually since the root cause and fix are singular (Finding 1).

§4-23 (universal window model / drag-reference) — surface matrix

Surface Pin (click) Split/lock (dblclick) Tray/popout objDrag source Notes
Ontology Manager — type rail N/T N/T N/T Code present (OntologyManagerScreen.tsx:282, {...objDrag(type.code, type.title)}) but unverifiable at runtime — 0 types exist (Finding 1), so the rail is permanently empty and there is nothing to click/drag. Code inspection only.
사용자 관리 (Users) table rows FAIL FAIL N/T FAIL — 0 [draggable="true"] elements on the page; grep confirms no objDrag/useWindowManager/usePinnedPanel import anywhere in UsersPage.tsx or src/console/identity/*.tsx. Clicking a row name does nothing (no pinned detail panel opens); it is plain text, not a button/objDrag source. Violates §4-20 ("모든 개체 표면 = 드래그 소스") for a live, populated, real object type (직원/사용자).
메신저 (Messenger) N/T N/T N/T UNVERIFIED MessengerConsoleScreen.tsx imports objDrag/window-model helpers per grep, but the scratch org has zero seeded threads/messages, so no chat bubble/chip existed to exercise. Not counted as a defect — environment data gap, not a confirmed code defect.
통합 개요 (Overview) — action inbox / todo list N/T N/T N/T UNVERIFIED Same — 0 rows (empty action inbox, empty todo list) in the scratch org; nothing to click or drag.

Legend: PASS / FAIL = actually exercised in the browser with real data. N/T = not tested (no reachable target in this environment/time window). UNVERIFIED = code exists but could not be exercised for lack of data, not counted as a confirmed defect.

Finding 2 — MEDIUM — 사용자 관리 (Users) rows are not object surfaces per §4-20/§4-23

Where: web/src/pages/UsersPage.tsx and web/src/console/identity/*.tsx.

Repro: log in as SUPER_ADMIN, navigate to /settings/users (a page with real, populated rows — the only surface in this audit with live data that wasn't blocked by Finding 1). Rows render as plain <td> text, not objDrag-wrapped chips/buttons; document.querySelectorAll('[draggable="true"]') returns 0 on the page. Clicking a row name does not open a pinned detail panel (§4.7 "상세 보기의 기본은 핀 패널이다"); it does nothing observable.

Impact: §4-20 requires every object-representing chip/row/code to be draggable and every detail-worthy row to open via the shared pin-panel click gesture. The user/employee object type — a first-class, universally present entity — does not participate in either. Medium severity: doesn't block other flows, but is a clear, easily-reproduced violation of a retroactive-audit directive on a live, non-empty surface.

Suggested fix direction: wrap the name cell in objDrag(user.code, user.name) and route the row click through the shared pin-panel affordance (usePinnedPanel/cardGrab) already used by the Ontology Manager's type rail, per §4-18's "중앙 정의 원칙" (reuse, don't reimplement).

Audit limitations (be upfront about scope)

  • Time/turn budget did not allow re-populating the scratch org with enough seed data (dispatch orders, tickets, messenger threads) to exercise every major surface listed in §4-23's directive — most surfaces were empty by construction (fresh migration, no fixtures), and creating that data through the console itself is blocked by Finding 1 for anything beyond the two cold-start users. Where a surface could not be reached with real data, it is marked N/T or UNVERIFIED above rather than guessed at.
  • No code was changed. Both findings above are new-charter-sized fixes (Finding 1 especially), not something to patch inline in this audit lane.