Skip to content

Commit 03c2f36

Browse files
authored
Merge pull request #83 from unisoncomputing/cp/update-project-permissions
Update all old project permissions filters
2 parents 30dc253 + 3833709 commit 03c2f36

11 files changed

Lines changed: 146 additions & 64 deletions

File tree

sql/2025-05-27_project-access.sql

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-- Function to check whether caller has access to a given project permission
2+
-- If the user_id is NULL, it returns TRUE for public projects
3+
CREATE FUNCTION user_has_project_permission(
4+
user_id UUID,
5+
project_id UUID,
6+
permission permission
7+
)
8+
RETURNS BOOLEAN
9+
STABLE
10+
PARALLEL SAFE
11+
AS $$
12+
SELECT EXISTS (
13+
SELECT
14+
FROM user_resource_permissions urp
15+
JOIN projects p ON urp.resource_id = p.resource_id
16+
WHERE (urp.user_id IS NULL OR urp.user_id = $1)
17+
AND p.id = $2
18+
AND urp.permission = $3
19+
);
20+
$$ LANGUAGE SQL;
21+
22+
-- DEPLOY NEW APP CODE HERE
23+
24+
-- Remove old project access management system
25+
-- We need to replace these with the new permissions system.
26+
DROP VIEW accessible_private_projects;
27+
DROP TABLE project_maintainers;
28+

src/Share/Postgres/Causal/Queries.hs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import Share.Postgres.Serialization qualified as S
5656
import Share.Postgres.Sync.Conversions qualified as Cv
5757
import Share.Prelude
5858
import Share.Utils.Postgres (OrdBy, ordered)
59+
import Share.Web.Authorization.Types (RolePermission (..))
5960
import Share.Web.Errors (MissingExpectedEntity (MissingExpectedEntity))
6061
import U.Codebase.Branch hiding (NamespaceStats, nonEmptyChildren)
6162
import U.Codebase.Branch qualified as V2 hiding (NamespaceStats)
@@ -936,33 +937,20 @@ importAccessibleCausals causalHashes = do
936937
JOIN causals ON causal_hashes.hash = causals.hash
937938
-- Ignore any causals that the codebase owner already has.
938939
WHERE NOT EXISTS (SELECT FROM causal_ownership co WHERE co.causal_id = causals.id AND co.user_id = #{codebaseOwnerUserId})
939-
), all_accessible_projects(owner, project_id) AS (
940-
(SELECT private_project.owner_user_id AS owner, private_project.project_id
941-
FROM accessible_private_projects private_project
942-
WHERE private_project.user_id = #{codebaseOwnerUserId}
943-
) UNION ALL
944-
(SELECT public_project.owner_user_id AS owner, public_project.id AS project_id
945-
FROM projects public_project
946-
WHERE NOT public_project.private
947-
)
948940
), copyable_causals(causal_id, causal_hash, project_owner, created_at) AS (
949-
SELECT cti.causal_id AS causal_id, cti.hash, project.owner, release.created_at
941+
SELECT cti.causal_id AS causal_id, cti.hash, project.owner_user_id, release.created_at
950942
FROM causals_to_import cti
951943
JOIN project_releases release ON release.squashed_causal_id = cti.causal_id
952-
JOIN all_accessible_projects project ON project.project_id = release.project_id
953-
-- This extra guarantee is required by the codebase migration from sqlite
954-
-- to PG, but doesn't hurt to keep around.
955-
JOIN causal_ownership ownership ON ownership.causal_id = cti.causal_id
956-
WHERE ownership.user_id = project.owner
944+
JOIN projects project ON project.id = release.project_id
945+
-- The caller must have permission to the view the project containing the causal
946+
WHERE user_has_project_permission(#{codebaseOwnerUserId}, release.project_id, #{ProjectView})
957947
UNION ALL
958-
SELECT cti.causal_id AS causal_id, cti.hash, project.owner, branch.created_at
948+
SELECT cti.causal_id AS causal_id, cti.hash, project.owner_user_id, branch.created_at
959949
FROM causals_to_import cti
960950
JOIN project_branches branch ON branch.causal_id = cti.causal_id
961-
JOIN all_accessible_projects project ON project.project_id = branch.project_id
962-
-- This extra guarantee is required by the codebase migration from sqlite
963-
-- to PG, but doesn't hurt to keep around.
964-
JOIN causal_ownership ownership ON ownership.causal_id = cti.causal_id
965-
WHERE ownership.user_id = project.owner
951+
JOIN projects project ON project.id = branch.project_id
952+
-- The caller must have permission to the view the project containing the causal
953+
WHERE user_has_project_permission(#{codebaseOwnerUserId}, branch.project_id, #{ProjectView})
966954
)
967955
-- Get only the first release (by created at) for each causal
968956
SELECT DISTINCT ON (copyable.causal_id) copyable.causal_id, copyable.project_owner

src/Share/Postgres/Contributions/Queries.hs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import Share.Postgres.Comments.Queries (commentsByTicketOrContribution)
3737
import Share.Postgres.IDs
3838
import Share.Prelude
3939
import Share.Utils.API
40+
import Share.Web.Authorization.Types (RolePermission (..))
4041
import Share.Web.Errors
4142
import Share.Web.Share.Contributions.API (ContributionTimelineCursor, ListContributionsCursor)
4243
import Share.Web.Share.Contributions.Types
@@ -304,13 +305,7 @@ listContributionsByUserId callerUserId userId limit mayCursor mayStatusFilter ma
304305
JOIN projects AS project ON project.id = contribution.project_id
305306
WHERE
306307
contribution.author_id = #{userId}
307-
AND NOT project.private
308-
OR EXISTS (
309-
SELECT FROM accessible_private_projects ap
310-
WHERE
311-
ap.user_id = #{callerUserId}
312-
AND ap.project_id = project.id
313-
)
308+
AND user_has_project_permission(#{callerUserId}, project.id, #{ProjectView})
314309
AND (#{mayStatusFilter} IS NULL OR contribution.status = #{mayStatusFilter})
315310
AND ^{cursorFilter}
316311
AND ^{kindFilter}

src/Share/Postgres/Queries.hs

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import Share.Ticket (TicketStatus)
3333
import Share.Ticket qualified as Ticket
3434
import Share.User
3535
import Share.Utils.API
36+
import Share.Web.Authorization.Types (RolePermission (..))
3637
import Share.Web.Errors (EntityMissing (EntityMissing), ErrorID (..))
3738
import Share.Web.Share.Branches.Types (BranchKindFilter (..))
3839
import Share.Web.Share.Projects.Types (ContributionStats (..), DownloadStats (..), FavData, ProjectOwner, TicketStats (..))
@@ -150,7 +151,7 @@ searchProjects caller (Just userId) (Query "") limit = do
150151
FROM projects p
151152
JOIN users owner ON p.owner_user_id = owner.id
152153
WHERE p.owner_user_id = #{userId}
153-
AND ((NOT p.private) OR (#{caller} IS NOT NULL AND EXISTS (SELECT FROM accessible_private_projects WHERE user_id = #{caller} AND project_id = p.id)))
154+
AND user_has_project_permission(#{caller}, p.id, #{ProjectView})
154155
ORDER BY p.created_at DESC
155156
LIMIT #{limit}
156157
|]
@@ -171,8 +172,8 @@ searchProjects caller userIdFilter (Query query) limit = do
171172
FROM to_tsquery('english', #{queryToken}) AS tokenquery, projects AS p
172173
JOIN users AS owner ON p.owner_user_id = owner.id
173174
WHERE (tokenquery @@ p.project_text_document OR p.slug ILIKE ('%' || like_escape(#{query}) || '%'))
174-
AND (NOT p.private OR (#{caller} IS NOT NULL AND EXISTS (SELECT FROM accessible_private_projects WHERE user_id = #{caller} AND project_id = p.id)))
175175
AND (#{userIdFilter} IS NULL OR p.owner_user_id = #{userIdFilter})
176+
AND user_has_project_permission(#{caller}, p.id, #{ProjectView})
176177
ORDER BY
177178
p.slug = #{query} DESC,
178179
-- Prefer prefix matches
@@ -278,12 +279,7 @@ listProjectsByUserWithMetadata callerUserId projectOwnerUserId = do
278279
FROM projects p
279280
JOIN users owner ON owner.id = p.owner_user_id
280281
WHERE p.owner_user_id = #{projectOwnerUserId}
281-
AND (EXISTS (SELECT FROM accessible_private_projects accessible
282-
WHERE accessible.user_id = #{callerUserId}
283-
AND accessible.project_id = p.id
284-
)
285-
OR NOT p.private
286-
)
282+
AND user_has_project_permission(#{callerUserId}, p.id, #{ProjectView})
287283
ORDER BY p.created_at DESC
288284
|]
289285
where
@@ -881,23 +877,6 @@ listContributorBranchesOfUserAccessibleToCaller contributorUserId mayCallerUserI
881877
let projectFilter = case mayProjectId of
882878
Nothing -> mempty
883879
Just projId -> [PG.sql| AND b.project_id = #{projId} |]
884-
let callerFilter = case mayCallerUserId of
885-
Just callerUserId ->
886-
-- See any projects that are public or that the caller has access to.
887-
( [PG.sql| AND (
888-
NOT project.private
889-
OR EXISTS (
890-
SELECT FROM accessible_private_projects ap
891-
WHERE
892-
ap.user_id = #{callerUserId}
893-
AND ap.project_id = project.id
894-
)
895-
)
896-
|]
897-
)
898-
Nothing ->
899-
-- No caller auth means they only see public projects
900-
[PG.sql| AND NOT project.private |]
901880
let sql =
902881
intercalateMap
903882
"\n"
@@ -930,11 +909,11 @@ listContributorBranchesOfUserAccessibleToCaller contributorUserId mayCallerUserI
930909
WHERE
931910
b.deleted_at IS NULL
932911
AND b.contributor_id = #{contributorUserId}
912+
AND user_has_project_permission(#{mayCallerUserId}, b.project_id, #{ProjectView})
933913
|],
934914
branchNameFilter,
935915
cursorFilter,
936916
projectFilter,
937-
callerFilter,
938917
[PG.sql|
939918
ORDER BY b.updated_at DESC, b.id DESC
940919
LIMIT #{limit}

src/Share/Postgres/Search/DefinitionSearch/Queries.hs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import Share.Postgres.Notifications qualified as Notif
3131
import Share.Prelude
3232
import Share.Utils.API (Limit, Query (Query))
3333
import Share.Utils.Logging qualified as Logging
34+
import Share.Web.Authorization.Types (RolePermission (..))
3435
import Share.Web.Errors qualified as Errors
3536
import Unison.DataDeclaration qualified as DD
3637
import Unison.Name (Name)
@@ -289,7 +290,8 @@ defNameCompletionSearch mayCaller mayFilter (Query query) limit = do
289290
WHERE
290291
-- Find names which contain the query
291292
doc.name ILIKE ('%.' || like_escape(#{query}) || '%')
292-
AND (NOT p.private OR (#{mayCaller} IS NOT NULL AND EXISTS (SELECT FROM accessible_private_projects pp WHERE pp.user_id = #{mayCaller} AND pp.project_id = p.id)))
293+
294+
AND user_has_project_permission(#{mayCaller}, p.id, #{ProjectView})
293295
^{filters}
294296
) SELECT r.name, r.tag FROM results r
295297
-- Docs and tests to the bottom, then
@@ -359,7 +361,7 @@ definitionTokenSearch mayCaller mayFilter limit searchTokens preferredArity = do
359361
WHERE
360362
-- match on search tokens using GIN index.
361363
tsquery(#{tsQueryText}) @@ doc.search_tokens
362-
AND (NOT p.private OR (#{mayCaller} IS NOT NULL AND EXISTS (SELECT FROM accessible_private_projects pp WHERE pp.user_id = #{mayCaller} AND pp.project_id = p.id)))
364+
AND user_has_project_permission(#{mayCaller}, p.id, #{ProjectView})
363365
AND (#{preferredArity} IS NULL OR doc.arity >= #{preferredArity})
364366
^{filters}
365367
^{namesFilter}
@@ -397,7 +399,7 @@ definitionNameSearch mayCaller mayFilter limit (Query query) = do
397399
WHERE
398400
-- We may wish to adjust the similarity threshold before the query.
399401
#{query} <% doc.name
400-
AND (NOT p.private OR (#{mayCaller} IS NOT NULL AND EXISTS (SELECT FROM accessible_private_projects pp WHERE pp.user_id = #{mayCaller} AND pp.project_id = p.id)))
402+
AND user_has_project_permission(#{mayCaller}, p.id, #{ProjectView})
401403
^{filters}
402404
-- Score matches by:
403405
-- - projects in the catalog

src/Share/Postgres/Tickets/Queries.hs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Share.Postgres qualified as PG
2626
import Share.Prelude
2727
import Share.Ticket (Ticket (..), TicketStatus)
2828
import Share.Utils.API
29+
import Share.Web.Authorization.Types (RolePermission (..))
2930
import Share.Web.Errors
3031
import Share.Web.Share.Comments
3132
import Share.Web.Share.Tickets.API
@@ -313,13 +314,7 @@ listTicketsByUserId callerUserId userId limit mayCursor mayStatusFilter = do
313314
JOIN projects AS project ON project.id = ticket.project_id
314315
WHERE
315316
ticket.author_id = #{userId}
316-
AND NOT project.private
317-
OR EXISTS (
318-
SELECT FROM accessible_private_projects ap
319-
WHERE
320-
ap.user_id = #{callerUserId}
321-
AND ap.project_id = project.id
322-
)
317+
AND user_has_project_permission(#{callerUserId}, project.id, #{ProjectView})
323318
AND (#{mayStatusFilter} IS NULL OR ticket.status = #{mayStatusFilter}::ticket_status)
324319
AND ^{cursorFilter}
325320
ORDER BY ticket.updated_at DESC, ticket.id DESC
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"body": [
3+
{
4+
"createdAt": "<TIMESTAMP>",
5+
"isFaved": false,
6+
"numFavs": 1,
7+
"owner": {
8+
"handle": "@test",
9+
"name": null,
10+
"type": "user"
11+
},
12+
"slug": "publictestproject",
13+
"summary": "test project summary",
14+
"tags": [],
15+
"updatedAt": "<TIMESTAMP>",
16+
"visibility": "public"
17+
},
18+
{
19+
"createdAt": "<TIMESTAMP>",
20+
"isFaved": false,
21+
"numFavs": 0,
22+
"owner": {
23+
"handle": "@test",
24+
"name": null,
25+
"type": "user"
26+
},
27+
"slug": "privatetestproject",
28+
"summary": "private summary",
29+
"tags": [],
30+
"updatedAt": "<TIMESTAMP>",
31+
"visibility": "private"
32+
}
33+
],
34+
"status": [
35+
{
36+
"status_code": 200
37+
}
38+
]
39+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"body": [
3+
{
4+
"createdAt": "<TIMESTAMP>",
5+
"isFaved": false,
6+
"numFavs": 1,
7+
"owner": {
8+
"handle": "@test",
9+
"name": null,
10+
"type": "user"
11+
},
12+
"slug": "publictestproject",
13+
"summary": "test project summary",
14+
"tags": [],
15+
"updatedAt": "<TIMESTAMP>",
16+
"visibility": "public"
17+
}
18+
],
19+
"status": [
20+
{
21+
"status_code": 200
22+
}
23+
]
24+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"body": [
3+
{
4+
"projectRef": "@test/privatetestproject",
5+
"summary": "private summary",
6+
"tag": "project",
7+
"visibility": "private"
8+
}
9+
],
10+
"status": [
11+
{
12+
"status_code": 200
13+
}
14+
]
15+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"body": [],
3+
"status": [
4+
{
5+
"status_code": 200
6+
}
7+
]
8+
}

0 commit comments

Comments
 (0)