feat(database): runnable RBAC role→policy backfill + upgrade-path E2E gate#277
Open
khuepm wants to merge 8 commits into
Open
feat(database): runnable RBAC role→policy backfill + upgrade-path E2E gate#277khuepm wants to merge 8 commits into
khuepm wants to merge 8 commits into
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
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.
apply|verify|rollback CLI; apply runs the post-check and exits non-zero
on failure.
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.
implementation and the CI gate instead of the hand-verified claim.