tenseleyflow/shithub / e54e752

Browse files

S28: policy.VisibilityPredicate — single-source SQL composer for visibility

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
e54e7520e6e739660dc5bc647744b178685bccae
Parents
8ed6f76
Tree
d0f8b75

1 changed file

StatusFile+-
A internal/auth/policy/visibility_predicate.go 75 0
internal/auth/policy/visibility_predicate.goadded
@@ -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
+}