feat(entitlements): per-product access control with admin + Stripe grants (BUNYIP-39) #51
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/bunyip-39-per-product-entitlements"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BUNYIP-39. Per-product entitlements on top of the M1 single-plan membership gate.
Model
A product with
requires_entitlement = falsebehaves exactly as before (open to every member). Flip it totrueto sell it as a tiered add-on. Chosen via the BUNYIP-39 scoping decisions: per-product flag (not all-or-nothing), and a migration that preserves current access.What ships (full feature)
Migration (
20260605000010):applications.requires_entitlement(default false),application_entitlements(user x product, revoked rows kept for audit),stripe_price_entitlements(price -> product mapping). Backfill grants every current member every active product, so flipping a product restricted later never retroactively locks out existing members.Domain:
ApplicationEntitlement/UserEntitlementRowmodels;EntitlementRepository(is_entitled, idempotent grant/revoke, revoke_all_for_user_by_source, list_for_user, price-mapping CRUD);ApplicationRepository::set_requires_entitlement.Enforcement (admins bypass everywhere): OCI token issuance +
get_manifest+get_blobrequire an entitlement for restricted products; the binary-download handler returns 403;/v1/downloadsand the per-app list hide restricted products the member is not entitled to. Denials audited (OciPullDeniedEntitlement,DownloadDeniedEntitlement).Admin API: grant / revoke / list per-user entitlements, mark a product restricted, manage the Stripe price -> product mapping; audited (
AdminEntitlementGranted/Revoked).Stripe: subscription created/updated re-syncs Stripe-sourced entitlements to the subscription's current prices (handles plan add/remove); canceled/deleted revokes them. Admin-granted entitlements (
source = 'admin') are never touched by Stripe sync, so a manual grant survives a billing event.Web: admin Entitlements page (per-product restricted toggle) + per-user grant/revoke view; an Entitlements link on each user row.
Verification
rust-builder-glibc 1.94.1 container:
clippy --workspace --all-targets -- -D warningsclean,fmt --all --checkclean,cargo test --workspace --lib206 passed.Live dev stack (migration applied: table + indexes + 7 backfill rows; all 7 seeded products default to open). Full enforcement matrix via the OCI
/auth/tokenendpoint for a freshly registered non-admin lifetime member (no backfill row):Notes / follow-ups
.sqlxregen needed). The access decision is enforced per request (membership AND entitlement are both re-checked), so a revoke takes effect within the OCI token TTL.