Skip to content

feat(database): runnable RBAC role→policy backfill + upgrade-path E2E gate#277

Open
khuepm wants to merge 8 commits into
mainfrom
claude/v1-evaluation-570dsb
Open

feat(database): runnable RBAC role→policy backfill + upgrade-path E2E gate#277
khuepm wants to merge 8 commits into
mainfrom
claude/v1-evaluation-570dsb

Conversation

@khuepm

@khuepm khuepm commented Jul 17, 2026

Copy link
Copy Markdown
Owner

The 0.6.x–0.17.x → 1.0 upgrade path promised a role-flag→policy backfill,
but the repo only carried pseudo-SQL in docs and an untested claim that the
verification 'has been run against the 1.0 release fixtures'. This makes the
path real and test-backed (v1 release criteria §4):

  • packages/database/src/backfill/role-policies.ts: idempotent backfill per
    the documented contract (exact flag copy, flag-only policy, priority 5,
    role flags never mutated), plus the zero-row post-check and the
    compat-window rollback, exported from @lumibase/database.
  • scripts/backfill-role-policies.ts (pnpm backfill:role-policies):
    apply|verify|rollback CLI; apply runs the post-check and exits non-zero
    on failure.
  • apps/cms/src/tests/upgrade-path.e2e.test.ts: seeds a pre-policy
    0.6.x-shape fixture against real Postgres and asserts backfill →
    zero-row post-check → exact flag preservation (admin-only/app-only not
    conflated) → idempotent re-run → clean rollback.
  • upgrades.md / role-policy-flag-migration.md: point at the shipped
    implementation and the CI gate instead of the hand-verified claim.

khuepm added 8 commits July 16, 2026 12:33
… gate

The 0.6.x–0.17.x → 1.0 upgrade path promised a role-flag→policy backfill,
but the repo only carried pseudo-SQL in docs and an untested claim that the
verification 'has been run against the 1.0 release fixtures'. This makes the
path real and test-backed (v1 release criteria §4):

- packages/database/src/backfill/role-policies.ts: idempotent backfill per
  the documented contract (exact flag copy, flag-only policy, priority 5,
  role flags never mutated), plus the zero-row post-check and the
  compat-window rollback, exported from @lumibase/database.
- scripts/backfill-role-policies.ts (pnpm backfill:role-policies):
  apply|verify|rollback CLI; apply runs the post-check and exits non-zero
  on failure.
- apps/cms/src/__tests__/upgrade-path.e2e.test.ts: seeds a pre-policy
  0.6.x-shape fixture against real Postgres and asserts backfill →
  zero-row post-check → exact flag preservation (admin-only/app-only not
  conflated) → idempotent re-run → clean rollback.
- upgrades.md / role-policy-flag-migration.md: point at the shipped
  implementation and the CI gate instead of the hand-verified claim.
…ted api chain

The Delivery API and the pageview beacon are anonymous-by-design (tenancy in
the URL), but both were mounted AFTER app.route('/api/v1', api). The api
sub-app's use('*') middleware flattens to /api/v1/*, and Hono runs matching
middleware registered before a handler — so every anonymous request died in
withTenant/withAuth (400 TENANT_REQUIRED / 401 UNAUTHENTICATED) before
reaching the handler. The other public surfaces (setup, shares, email
unsubscribe) already sat before the api mount, which is why only these two
were dead.

- Move both public mounts above the api mount (same disjoint-leaf mechanism
  the shares/email mounts rely on) and scope the beacon middleware to the
  /:site_id/hit leaf so the authenticated /pageviews/stats doesn't run
  withDb/withRateLimit twice.
- Extend the golden-path E2E with the anonymous leg that caught this: a
  credential-less Delivery API read of the published item (v1 release
  criteria §3 'read via public API'), plus a tenant-isolation assert on the
  anonymous surface.
…ease targets

- ci.yml: run the new upgrade-path E2E after the golden path in the
  e2e-golden-path job (sequential on purpose — shared database); drop the
  duplicated Typecheck step, keeping the pnpm -r variant so no turbo cache
  can mask a type error.
- release.yml: assert the release itself, not just the side workflows
  (v1 release criteria §3 — smoke-test both deployment targets on the RC):
  post-deploy health + served-version check against the production Worker,
  and a new smoke-docker-image job that pulls the exact ghcr image pushed
  for the tag and asserts /health answers.
- .kiro/steering/v1-scope-classification.md: official in-v1 / post-v1
  classification for all 35 specs (release criteria §2) — 23 complete,
  6 complete-with-deferrals (items listed as 1.x backlog), 1 partial
  (deployment-integrations, declared post-v1 hardening), 5 not-started
  proposals. Linked from v1-release-criteria.md.
- cwe-top-100-audit.md: explicit 'Accepted for v1.0' verdict on the
  CWE-359 erasure-vs-audit-log gap — policy is redact-on-write, so audit
  metadata never carries plaintext PII; sweeping audit logs in erasure
  stays on the post-1.0 hardening list.
Security review of the v0.18.0→HEAD release diff (v1 criteria §1c) flagged a
privilege-escalation path introduced by the backfill: deriveLegacyRoleKey
normalized `systemKey ?? key ?? id` lossily, so two distinct roles in one
site (e.g. `content-editor` and `Content Editor`) derived the same policy
key. The onConflictDoUpdate then merged them onto one policy row and linked
both roles to it — during the compat window (role flags OR policy flags) a
low-privilege role could inherit a colliding admin role's admin_access.

- Suffix the stable role id onto the derived key so the role↔policy mapping
  is guaranteed 1:1; collisions are structurally impossible. Safe to change
  the key scheme now since the backfill has never shipped.
- upgrade-path E2E: add a collision pair (two roles whose keys normalize to
  the same readable string, differing flags) and assert they get separate
  policies and the app-only role never picks up admin_access.
- Docs updated to document the id suffix and why it's required.
Security review of the release surface found POST /api/v1/marketplace/publish
had no permission or ownership check (unlike every sibling write route), so
any authenticated user with any site membership could publish or overwrite
global catalog rows, skip community moderation, and spoof the 'verified'
badge by supplying signature fields. (Pre-existing since the signed-marketplace
feature; surfaced by the v1 release review.)

- Gate /publish with requireConfigurePermission (extensions:configure), the
  same moderator capability as /submissions/review.
- Verify the supplied signature against the STORED bundle via
  ExtensionVerifierService before persisting; server-derive isOfficial /
  verifiedAt (never from the client). Reserved lumibase-* must be signed by
  an official key, mirroring the install-time gate.
- Refuse to publish a community submission whose submissionStatus is not
  'approved' (direct-publish official rows have null status and are exempt).
- Tests: convert the mock-based perf test (which encoded the old unauthed
  contract) to the real-DB harness and assert the batched fan-out; add
  regression cases for the 403 (no configure) and 409 (unapproved) paths.
- llms.txt: replace the pre-v0.5.0 'headless CMS + AI Copilot' summary (the
  canonical text AI crawlers read) with the current Content OS framing —
  governed agents, declarative SLOs, trust ladder L0-L4, provenance, MCP
  server, CDC.
- opengraph-image: drop the abandoned green '>_' terminal mark for the live
  violet sphere brand (the most-shared visual asset was off-brand).
- Legal: correct the MIT header copyright to 2025-2026; hardcode the privacy
  and ToS 'Last updated' dates (were new Date() → always 'today').
- Fix the broken 'Content OS vision' footer link (/en/ai-native-vision.md →
  /en/docs/ai-native-vision, matching the docs :locale/docs/* route) and
  align the Twitter card handle with the account actually linked.
- README: correct the 'automatic dark mode' claim (dark-only) and refresh the
  component/route tree; delete the unused, off-brand ControlLoop component.
The /rewards/claim page POSTed to /api/rewards/claim, but that API lives under
_api-routes-disabled and the site is a static export (output: 'export') — so
the endpoint never exists and every claim attempt failed. The pricing FAQ
advertised this non-functional token-unlock flow.

- Delete src/app/rewards/claim (dead, always-failing form).
- Rewrite the 'How does GitHub Sponsors work?' FAQ to describe the real flow
  (sponsorship funds development; managed tiers arranged directly; core is
  free, no token) instead of promising an automated reward token.

The 'Sponsor on GitHub' CTA stays — GitHub Sponsors itself is real. The
disabled API stub + lib/rewards.ts are left in place (not shipped) for if the
managed backend is built later.
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.

1 participant