package services // Application-layer mirror of paliad.can_see_project (defined in migration // 023). The SQL function is enforced by RLS, but services run with a // service-role connection that has no auth.uid() — so each query that // returns project-scoped rows re-applies the same predicate inline. // // Predicate shape (matches can_see_project body): // // global_admin in paliad.users OR any (direct or ancestor) team membership // // We resolve the global-admin shortcut by EXISTS-querying paliad.users on // the supplied userID rather than asking callers to pass user.GlobalRole as // a separate parameter. That eliminates an entire bug class (mismatched // role string, e.g. checking 'admin' when the column stores 'global_admin') // and means the only argument the predicate needs is the user UUID. // // Callers must JOIN paliad.projects under the given alias so the path-walk // half of the predicate can read alias.path. import "fmt" // visibilityPredicate returns the predicate using sqlx named bind variables. // Caller binds :user_id to the requesting user's UUID. // // Uses CAST(... AS uuid[]) — not the `::uuid[]` shorthand — because sqlx // v1.4.0's named-parameter parser strips one colon from `::uuid[]` while // compiling `:user_id` placeholders, producing invalid SQL `:uuid[]` that // Postgres rejects with `syntax error at or near ":"`. func visibilityPredicate(alias string) string { return `(EXISTS ( SELECT 1 FROM paliad.users u WHERE u.id = :user_id AND u.global_role = 'global_admin' ) OR EXISTS ( SELECT 1 FROM paliad.project_teams pt WHERE pt.user_id = :user_id AND pt.project_id = ANY(CAST(string_to_array(` + alias + `.path, '.') AS uuid[])) ))` } // visibilityPredicatePositional returns the predicate with one positional // placeholder ($userArg) for the user UUID. Use when the surrounding query // can't take named parameters (mixed positional args, lib/pq array binding, // etc.). The same $userArg is referenced twice — that is a feature of // Postgres positional binding, not a bug. func visibilityPredicatePositional(alias string, userArg int) string { return fmt.Sprintf(`(EXISTS ( SELECT 1 FROM paliad.users u WHERE u.id = $%d AND u.global_role = 'global_admin' ) OR EXISTS ( SELECT 1 FROM paliad.project_teams pt WHERE pt.user_id = $%d AND pt.project_id = ANY(string_to_array(%s.path, '.')::uuid[]) ))`, userArg, userArg, alias) } // projectDescendantPredicate matches rows whose joined `paliad.projects` // row (under `alias`) is the project bound to :project_id OR any descendant // of it. The check inspects the materialised path column on paliad.projects // — :project_id appears in the path of every descendant, plus the project // itself (since path always includes self as the last label, t-paliad-018). // // Uses CAST(... AS uuid[]) — not the `::uuid[]` shorthand — for the same // sqlx-named-parameter reason called out on visibilityPredicate above. // // Caller must JOIN paliad.projects under the given alias and bind // :project_id to the selected project's UUID. func projectDescendantPredicate(alias string) string { return `:project_id = ANY(CAST(string_to_array(` + alias + `.path, '.') AS uuid[]))` }