@@ -107,6 +107,30 @@ the verdict. Ordered from most-decisive to least: |
| 107 | 107 | 10. **Login-required actions** (star/fork) on anonymous → deny |
| 108 | 108 | (`DenyAnonymous`). |
| 109 | 109 | |
| 110 | +## Authorization versus entitlements |
| 111 | + |
| 112 | +`policy.Can` answers only one question: is this actor allowed to |
| 113 | +perform this action on this resource under shithub's permission model? |
| 114 | +It must not decide whether an organization has paid for a feature. |
| 115 | + |
| 116 | +Paid organization checks are a second gate after authorization. The |
| 117 | +expected flow for gated writes is: |
| 118 | + |
| 119 | +1. Load the resource and run the normal policy check. |
| 120 | +2. Preserve `policy.Maybe404` behavior for private-resource denials. |
| 121 | +3. Ask the entitlement layer whether the organization has the specific |
| 122 | + feature key. |
| 123 | +4. If the feature is unavailable, return a billing/upgrade response |
| 124 | + without re-deriving ownership, visibility, role, or plan state in |
| 125 | + the handler. |
| 126 | + |
| 127 | +The entitlement layer may inspect billing state and plan-derived |
| 128 | +features. Policy code, handlers, git transports, and domain packages |
| 129 | +must not branch directly on `orgs.plan` or sqlc `OrgPlan*` constants. |
| 130 | +That keeps security authorization independent from commercial product |
| 131 | +packaging, and makes downgrades/grace periods possible without |
| 132 | +rewriting role checks. |
| 133 | + |
| 110 | 134 | ## Existence-leak guard |
| 111 | 135 | |
| 112 | 136 | `policy.Maybe404(decision, repo, actor)` maps a denial to a status |
@@ -212,3 +236,9 @@ the policy actor at the lookup wrapper): |
| 212 | 236 | Test files everywhere are exempt — they legitimately seed state. If a |
| 213 | 237 | new pattern surfaces (e.g. an issue handler reads `issue.author_id`), |
| 214 | 238 | extend the script accordingly. |
| 239 | + |
| 240 | +`scripts/lint-org-plan-boundary.sh` also runs in `make ci`. It fails on |
| 241 | +direct plan feature gates outside `internal/billing/`, |
| 242 | +`internal/entitlements/`, generated sqlc models, migrations, and tests. |
| 243 | +When adding a paid feature, add or use an entitlement feature key rather |
| 244 | +than comparing `OrgPlanTeam` or `OrgPlanEnterprise` at the call site. |