@@ -0,0 +1,71 @@ |
| | 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. |
| | 33 | +PATTERNS=( |
| | 34 | + '\.OwnerUserID == ' |
| | 35 | + '\.OwnerUserID\.Int64 == ' |
| | 36 | + '== .*\.OwnerUserID' |
| | 37 | + '\.Visibility == .*RepoVisibility' |
| | 38 | + 'if .*\.IsArchived ' |
| | 39 | +) |
| | 40 | + |
| | 41 | +# Files we're guarding — anywhere a request handler or hook lives. |
| | 42 | +INCLUDE=( |
| | 43 | + ':!internal/auth/policy/*' |
| | 44 | + ':!internal/repos/*' |
| | 45 | + ':!internal/web/handlers/repo/*' |
| | 46 | + ':!**/*_test.go' |
| | 47 | +) |
| | 48 | + |
| | 49 | +violations=0 |
| | 50 | +for pat in "${PATTERNS[@]}"; do |
| | 51 | + matches=$(git grep -nE "$pat" -- \ |
| | 52 | + 'internal/web/handlers' 'internal/git' 'cmd/shithubd' \ |
| | 53 | + "${INCLUDE[@]}" 2>/dev/null || true) |
| | 54 | + if [[ -n "$matches" ]]; then |
| | 55 | + echo "policy boundary violation — pattern: $pat" |
| | 56 | + echo "$matches" | sed 's/^/ /' |
| | 57 | + echo |
| | 58 | + violations=1 |
| | 59 | + fi |
| | 60 | +done |
| | 61 | + |
| | 62 | +if [[ "$violations" -ne 0 ]]; then |
| | 63 | + echo "------------------------------------------------------------" |
| | 64 | + echo "Authorization checks must flow through internal/auth/policy." |
| | 65 | + echo "If your case is legitimate data plumbing (e.g. a sqlc.Params" |
| | 66 | + echo "field), refactor to read the value from a struct field rather" |
| | 67 | + echo "than compare inline." |
| | 68 | + echo "------------------------------------------------------------" |
| | 69 | + exit 1 |
| | 70 | +fi |
| | 71 | +echo "policy boundary: clean" |