| 1 | #!/usr/bin/env bash |
| 2 | # SPDX-License-Identifier: AGPL-3.0-or-later |
| 3 | # |
| 4 | # Fail when authorization decisions leak outside internal/auth/policy. |
| 5 | # After S15, every "is this actor allowed to do X?" check must flow |
| 6 | # through policy.Can(); handler/git/cmd code must not branch on |
| 7 | # ownership, visibility, or collaborator state inline. |
| 8 | # |
| 9 | # The check looks for these smells in handler/git/hook code: |
| 10 | # - `OwnerUserID == ` or `== row.OwnerUserID` style equality compares |
| 11 | # - `Visibility == reposdb.Repo` equality compares |
| 12 | # - `IsArchived` used as a control-flow predicate (not a parameter) |
| 13 | # |
| 14 | # Pure data-plumbing references (constructing sqlc.Params, returning |
| 15 | # the column from a query, logging) are allowed — those don't decide |
| 16 | # anything. |
| 17 | # |
| 18 | # Allowed locations (carved out below): |
| 19 | # internal/auth/policy/... |
| 20 | # internal/repos/... (repo-create + repo orchestration) |
| 21 | # internal/web/handlers/repo/ (constructs the policy actor; lookup |
| 22 | # wrapper is policy-aware) |
| 23 | # *_test.go everywhere (tests legitimately seed state) |
| 24 | # |
| 25 | # The script exits 0 when no violations are found, 1 otherwise. Run as |
| 26 | # part of `make ci`. |
| 27 | |
| 28 | set -euo pipefail |
| 29 | |
| 30 | cd "$(git rev-parse --show-toplevel)" |
| 31 | |
| 32 | # Patterns that smell like an inline auth decision. The audit found |
| 33 | # negation forms (`!=`) slipping past the original equality-only set, |
| 34 | # so both directions are covered. Same for the visibility shape — both |
| 35 | # `== "private"` literal and the typed-enum compare. |
| 36 | PATTERNS=( |
| 37 | '\.OwnerUserID == ' |
| 38 | '\.OwnerUserID != ' |
| 39 | '\.OwnerUserID\.Int64 == ' |
| 40 | '\.OwnerUserID\.Int64 != ' |
| 41 | '== .*\.OwnerUserID' |
| 42 | '!= .*\.OwnerUserID' |
| 43 | '\.Visibility == .*RepoVisibility' |
| 44 | '\.Visibility != .*RepoVisibility' |
| 45 | '\.Visibility == "(public|private)"' |
| 46 | '\.Visibility != "(public|private)"' |
| 47 | 'if .*\.IsArchived ' |
| 48 | 'if !.*\.IsArchived ' |
| 49 | ) |
| 50 | |
| 51 | # Files we're guarding — anywhere a request handler or hook lives. |
| 52 | INCLUDE=( |
| 53 | ':!internal/auth/policy/*' |
| 54 | ':!internal/repos/*' |
| 55 | ':!internal/web/handlers/repo/*' |
| 56 | ':!**/*_test.go' |
| 57 | ) |
| 58 | |
| 59 | violations=0 |
| 60 | for pat in "${PATTERNS[@]}"; do |
| 61 | matches=$(git grep -nE "$pat" -- \ |
| 62 | 'internal/web/handlers' 'internal/git' 'cmd/shithubd' \ |
| 63 | "${INCLUDE[@]}" 2>/dev/null || true) |
| 64 | if [[ -n "$matches" ]]; then |
| 65 | echo "policy boundary violation — pattern: $pat" |
| 66 | echo "$matches" | sed 's/^/ /' |
| 67 | echo |
| 68 | violations=1 |
| 69 | fi |
| 70 | done |
| 71 | |
| 72 | if [[ "$violations" -ne 0 ]]; then |
| 73 | echo "------------------------------------------------------------" |
| 74 | echo "Authorization checks must flow through internal/auth/policy." |
| 75 | echo "If your case is legitimate data plumbing (e.g. a sqlc.Params" |
| 76 | echo "field), refactor to read the value from a struct field rather" |
| 77 | echo "than compare inline." |
| 78 | echo "------------------------------------------------------------" |
| 79 | exit 1 |
| 80 | fi |
| 81 | echo "policy boundary: clean" |