| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | // Package policy is the single source of truth for "who can do what". |
| 4 | // Every authorization decision in shithub flows through Can(); handlers |
| 5 | // must not read ownership, visibility, or collaborator state inline. |
| 6 | // |
| 7 | // The package shape: |
| 8 | // |
| 9 | // Actor — who is asking (anonymous, suspended, site-admin etc.) |
| 10 | // Resource — what they want to act on (a RepoRef today; org/team later) |
| 11 | // Action — a constant from the registry below |
| 12 | // Can(...) — the only public decision function |
| 13 | // Decision — { Allow bool; Reason string } |
| 14 | // |
| 15 | // Reasons are for logs and admin debugging, never for end-user error |
| 16 | // strings — they can leak existence. |
| 17 | package policy |
| 18 | |
| 19 | // Action is a constant identifier for an operation against some resource. |
| 20 | // Adding a new action: add the const, register a default rule in |
| 21 | // policy.Can, and extend the test matrix. The matrix test asserts that |
| 22 | // every Action has explicit coverage for every Actor archetype. |
| 23 | type Action string |
| 24 | |
| 25 | // Repo-level actions. |
| 26 | const ( |
| 27 | ActionRepoRead Action = "repo:read" |
| 28 | ActionRepoWrite Action = "repo:write" |
| 29 | ActionRepoAdmin Action = "repo:admin" |
| 30 | |
| 31 | ActionRepoSettingsGeneral Action = "repo:settings:general" |
| 32 | ActionRepoSettingsCollaborators Action = "repo:settings:collaborators" |
| 33 | ActionRepoSettingsBranches Action = "repo:settings:branches" |
| 34 | ActionRepoSettingsActions Action = "repo:settings:actions" |
| 35 | |
| 36 | ActionRepoArchive Action = "repo:archive" |
| 37 | ActionRepoDelete Action = "repo:delete" |
| 38 | ActionRepoTransfer Action = "repo:transfer" |
| 39 | ActionRepoVisibility Action = "repo:visibility" |
| 40 | |
| 41 | ActionActionsRun Action = "actions:run" |
| 42 | ActionActionsApprove Action = "actions:approve" |
| 43 | ) |
| 44 | |
| 45 | // Issue-level actions. (Issue resources arrive in S18; S15 just ships |
| 46 | // the registry entries so the matrix is exhaustive from day one.) |
| 47 | const ( |
| 48 | ActionIssueRead Action = "issue:read" |
| 49 | ActionIssueCreate Action = "issue:create" |
| 50 | ActionIssueComment Action = "issue:comment" |
| 51 | ActionIssueClose Action = "issue:close" |
| 52 | ActionIssueLabel Action = "issue:label" |
| 53 | ActionIssueAssign Action = "issue:assign" |
| 54 | ) |
| 55 | |
| 56 | // Pull-request actions. (Pull resources arrive in S19; same note as above.) |
| 57 | const ( |
| 58 | ActionPullRead Action = "pull:read" |
| 59 | ActionPullCreate Action = "pull:create" |
| 60 | ActionPullMerge Action = "pull:merge" |
| 61 | ActionPullReview Action = "pull:review" |
| 62 | ActionPullClose Action = "pull:close" |
| 63 | ) |
| 64 | |
| 65 | // Per-user social actions. |
| 66 | const ( |
| 67 | ActionStarCreate Action = "star:create" |
| 68 | ActionForkCreate Action = "fork:create" |
| 69 | |
| 70 | // S26 social actions. WatchSet covers both setting an explicit |
| 71 | // level and unsetting (deleting the row). Both require a logged-in |
| 72 | // user with read access to the repo — the policy.Can engine |
| 73 | // enforces visibility before reaching the role check, so a |
| 74 | // non-collab on a private repo deny-leaks as 404. |
| 75 | ActionWatchSet Action = "watch:set" |
| 76 | ) |
| 77 | |
| 78 | // AllActions is the canonical list. The matrix test iterates this so a |
| 79 | // new Action that's not registered above will fail coverage and force |
| 80 | // the author to think through every actor archetype. |
| 81 | var AllActions = []Action{ |
| 82 | ActionRepoRead, ActionRepoWrite, ActionRepoAdmin, |
| 83 | ActionRepoSettingsGeneral, ActionRepoSettingsCollaborators, ActionRepoSettingsBranches, ActionRepoSettingsActions, |
| 84 | ActionRepoArchive, ActionRepoDelete, ActionRepoTransfer, ActionRepoVisibility, |
| 85 | ActionActionsRun, ActionActionsApprove, |
| 86 | ActionIssueRead, ActionIssueCreate, ActionIssueComment, ActionIssueClose, ActionIssueLabel, ActionIssueAssign, |
| 87 | ActionPullRead, ActionPullCreate, ActionPullMerge, ActionPullReview, ActionPullClose, |
| 88 | ActionStarCreate, ActionForkCreate, |
| 89 | ActionWatchSet, |
| 90 | } |
| 91 | |
| 92 | // isWriteAction returns true when the action mutates state. Used by the |
| 93 | // suspended-user gate (suspended accounts can read but not write). |
| 94 | func isWriteAction(a Action) bool { |
| 95 | switch a { |
| 96 | case ActionRepoRead, ActionIssueRead, ActionPullRead: |
| 97 | return false |
| 98 | default: |
| 99 | return true |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | // isReadAction is the inverse, broken out for readability at call sites |
| 104 | // that branch on intent rather than on the absence of writes. |
| 105 | func isReadAction(a Action) bool { return !isWriteAction(a) } |
| 106 | |
| 107 | // isIssueParticipationAction is the GitHub-shaped issue conversation |
| 108 | // surface: any logged-in user can open or comment on issues in a public |
| 109 | // repo, while private repos require normal read access. |
| 110 | func isIssueParticipationAction(a Action) bool { |
| 111 | return a == ActionIssueCreate || a == ActionIssueComment |
| 112 | } |
| 113 |