| 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 | } |
| 76 |