@@ -0,0 +1,75 @@ |
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | + |
| 3 | +package policy |
| 4 | + |
| 5 | +import "fmt" |
| 6 | + |
| 7 | +// VisibilityPredicate returns a SQL WHERE-clause fragment plus its |
| 8 | +// bind args that filters rows from a `repos` table reference (or a |
| 9 | +// table that joins to repos via a column named like |
| 10 | +// `repo_id`/`r.id`) to those visible to the actor. |
| 11 | +// |
| 12 | +// The fragment is parameterised against the supplied placeholder |
| 13 | +// offset so callers can splice it into queries that already bind |
| 14 | +// other parameters. Returns: |
| 15 | +// |
| 16 | +// clause — SQL fragment ready to drop after `WHERE` or `AND`. |
| 17 | +// args — the values for the placeholders inside `clause`, in |
| 18 | +// order from `$<startPlaceholder>` upward. |
| 19 | +// |
| 20 | +// `tableAlias` is the alias the caller used for the `repos` table |
| 21 | +// (e.g. "r" for `FROM repos r`). The fragment references |
| 22 | +// `<tableAlias>.visibility`, `<tableAlias>.owner_user_id`, |
| 23 | +// `<tableAlias>.deleted_at`, and `<tableAlias>.id` as needed. |
| 24 | +// |
| 25 | +// Visibility rules (mirror policy.Can step 4–6): |
| 26 | +// |
| 27 | +// - Soft-deleted repos are always excluded. |
| 28 | +// - Public repos visible to anyone. |
| 29 | +// - Private repos visible only to: owner, or any user with a |
| 30 | +// row in `repo_collaborators` (any role). |
| 31 | +// |
| 32 | +// Site-admin special-cased: when actor.IsSiteAdmin, only the |
| 33 | +// soft-delete filter applies (admins can read everything). |
| 34 | +// |
| 35 | +// This is the single source of truth for "what repos can this |
| 36 | +// viewer see in a list query". S28 search composes it; future |
| 37 | +// listing endpoints (trending, activity feed) reuse it. |
| 38 | +func VisibilityPredicate(actor Actor, tableAlias string, startPlaceholder int) (clause string, args []any) { |
| 39 | + if tableAlias == "" { |
| 40 | + tableAlias = "r" |
| 41 | + } |
| 42 | + |
| 43 | + // Always exclude soft-deleted. |
| 44 | + base := fmt.Sprintf("%s.deleted_at IS NULL", tableAlias) |
| 45 | + |
| 46 | + if actor.IsSiteAdmin { |
| 47 | + // Admins see everything that isn't soft-deleted. |
| 48 | + return base, nil |
| 49 | + } |
| 50 | + |
| 51 | + if actor.IsAnonymous { |
| 52 | + // Public only. |
| 53 | + return fmt.Sprintf( |
| 54 | + "%s AND %s.visibility = 'public'", |
| 55 | + base, tableAlias, |
| 56 | + ), nil |
| 57 | + } |
| 58 | + |
| 59 | + // Logged-in: public OR (owner) OR (collab row exists). |
| 60 | + // Two placeholders consumed: actor.UserID twice (owner check + |
| 61 | + // collab subquery). |
| 62 | + p1 := startPlaceholder |
| 63 | + p2 := startPlaceholder + 1 |
| 64 | + clause = fmt.Sprintf( |
| 65 | + "%s AND ("+ |
| 66 | + "%s.visibility = 'public' "+ |
| 67 | + "OR %s.owner_user_id = $%d "+ |
| 68 | + "OR EXISTS (SELECT 1 FROM repo_collaborators c "+ |
| 69 | + "WHERE c.repo_id = %s.id AND c.user_id = $%d)"+ |
| 70 | + ")", |
| 71 | + base, tableAlias, tableAlias, p1, tableAlias, p2, |
| 72 | + ) |
| 73 | + args = []any{actor.UserID, actor.UserID} |
| 74 | + return clause, args |
| 75 | +} |