Guard org plan entitlement boundary
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
5b1a1f3abc70470be3c383148d9770525cc88a06- Parents
-
bb04500 - Tree
93227cb
5b1a1f3
5b1a1f3abc70470be3c383148d9770525cc88a06bb04500
93227cb| Status | File | + | - |
|---|---|---|---|
| M |
Makefile
|
5 | 2 |
| M |
docs/internal/billing.md
|
4 | 0 |
| M |
docs/internal/security-checklist.md
|
1 | 0 |
| A |
scripts/lint-org-plan-boundary.sh
|
41 | 0 |
Makefilemodified@@ -2,7 +2,7 @@ | ||
| 2 | 2 | # Targets mirror what CI runs. The Makefile is the source of truth. |
| 3 | 3 | |
| 4 | 4 | .DEFAULT_GOAL := help |
| 5 | -.PHONY: help dev build test test-race lint lint-policy lint-markdown lint-secret-logs lint-spdx lint-unused lint-migrations verify-api-docs fmt tidy clean ci assets install-tools version deploy deploy-check restore-drill bench-staging docs docs-serve docs-verify gen-third-party-notices audit-a11y audit-a11y-pa11y audit-a11y-axe load-test | |
| 5 | +.PHONY: help dev build test test-race lint lint-policy lint-markdown lint-org-plan lint-secret-logs lint-spdx lint-unused lint-migrations verify-api-docs fmt tidy clean ci assets install-tools version deploy deploy-check restore-drill bench-staging docs docs-serve docs-verify gen-third-party-notices audit-a11y audit-a11y-pa11y audit-a11y-axe load-test | |
| 6 | 6 | |
| 7 | 7 | # Build metadata embedded into the binary via -ldflags. |
| 8 | 8 | VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) |
@@ -72,7 +72,7 @@ assets: ## Copy Primer CSS into internal/web/static/ for embedding. | ||
| 72 | 72 | echo "warn: .refs/primer-css/dist not found; run 'git clone https://github.com/primer/css .refs/primer-css' first"; \ |
| 73 | 73 | fi |
| 74 | 74 | |
| 75 | -ci: lint lint-policy lint-markdown lint-secret-logs lint-spdx lint-unused lint-migrations verify-api-docs test build ## Full CI pipeline (matches .github/workflows/ci.yml). | |
| 75 | +ci: lint lint-policy lint-markdown lint-org-plan lint-secret-logs lint-spdx lint-unused lint-migrations verify-api-docs test build ## Full CI pipeline (matches .github/workflows/ci.yml). | |
| 76 | 76 | @echo "ci: ok" |
| 77 | 77 | |
| 78 | 78 | lint-policy: ## Enforce policy-package boundary (no inline auth checks in handlers/git/cmd). |
@@ -81,6 +81,9 @@ lint-policy: ## Enforce policy-package boundary (no inline auth checks in handle | ||
| 81 | 81 | lint-markdown: ## Enforce markdown-package boundary (no goldmark/bluemonday outside internal/markdown). |
| 82 | 82 | @scripts/lint-markdown-boundary.sh |
| 83 | 83 | |
| 84 | +lint-org-plan: ## Enforce paid org entitlement boundary (no direct orgs.plan feature gates). | |
| 85 | + @scripts/lint-org-plan-boundary.sh | |
| 86 | + | |
| 84 | 87 | lint-secret-logs: ## Fail when source emits log lines containing token-prefix patterns. |
| 85 | 88 | @scripts/lint-secret-logs.sh |
| 86 | 89 | |
docs/internal/billing.mdmodified@@ -111,6 +111,10 @@ Required local concepts: | ||
| 111 | 111 | Paid feature checks must live behind a central entitlement package, not |
| 112 | 112 | as scattered `orgs.plan` checks in handlers. |
| 113 | 113 | |
| 114 | +`make lint-org-plan` enforces this boundary. Schema/sqlc plumbing may | |
| 115 | +store and scan the plan value, but product behavior should ask the | |
| 116 | +entitlement package whether a feature key is available. | |
| 117 | + | |
| 114 | 118 | Expected feature keys: |
| 115 | 119 | |
| 116 | 120 | - `org.secret_teams` |
docs/internal/security-checklist.mdmodified@@ -35,6 +35,7 @@ document. | ||
| 35 | 35 | | Org suspension blocks writes | `policy.Can` + `DenyOrgSuspended` | S30 | |
| 36 | 36 | | Repo soft-delete blocks all actions | `policy.Can` + `DenyRepoDeleted` | S15 | |
| 37 | 37 | | Author-self-close on issues/PRs | `policy.Can` author branch | S21/S22 | |
| 38 | +| Paid org feature gates stay behind entitlements | `scripts/lint-org-plan-boundary.sh` in `make ci` | PAYMENTS SP01 | | |
| 38 | 39 | |
| 39 | 40 | ## Input handling |
| 40 | 41 | |
scripts/lint-org-plan-boundary.shadded@@ -0,0 +1,41 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# SPDX-License-Identifier: AGPL-3.0-or-later | |
| 3 | +# | |
| 4 | +# Fail when paid-organization feature decisions branch directly on | |
| 5 | +# orgs.plan outside the billing/entitlement boundary. Plan storage is | |
| 6 | +# allowed in schema/sqlc plumbing, but handlers and domain packages | |
| 7 | +# should ask the entitlement layer whether a feature is available. | |
| 8 | +# | |
| 9 | +# Allowed locations: | |
| 10 | +# internal/billing/... — owns subscription state | |
| 11 | +# internal/entitlements/... — owns feature availability | |
| 12 | +# internal/*/sqlc/... — generated data models | |
| 13 | +# internal/migrationsfs/... — schema definitions | |
| 14 | +# *_test.go everywhere — tests may seed or assert plan values | |
| 15 | +# | |
| 16 | +# Run from `make ci`. | |
| 17 | + | |
| 18 | +set -euo pipefail | |
| 19 | + | |
| 20 | +cd "$(git rev-parse --show-toplevel)" | |
| 21 | + | |
| 22 | +PATTERN='OrgPlan(Free|Team|Enterprise)|\.Plan[[:space:]]*(==|!=)|orgs\.plan|plan[[:space:]]+org_plan' | |
| 23 | + | |
| 24 | +matches=$(git grep -nE "$PATTERN" -- \ | |
| 25 | + 'cmd' 'internal' \ | |
| 26 | + ':!internal/billing/*' \ | |
| 27 | + ':!internal/entitlements/*' \ | |
| 28 | + ':!internal/*/sqlc/*' \ | |
| 29 | + ':!internal/migrationsfs/*' \ | |
| 30 | + ':!**/*_test.go' \ | |
| 31 | + 2>/dev/null || true) | |
| 32 | + | |
| 33 | +if [[ -n "$matches" ]]; then | |
| 34 | + echo "lint-org-plan-boundary: direct org plan feature gate outside billing/entitlements:" >&2 | |
| 35 | + echo "$matches" | sed 's/^/ /' >&2 | |
| 36 | + echo "" >&2 | |
| 37 | + echo "Fix: add or use an entitlement feature check instead of branching on orgs.plan directly." >&2 | |
| 38 | + exit 1 | |
| 39 | +fi | |
| 40 | + | |
| 41 | +echo "lint-org-plan-boundary: ok" | |