Skip to content

Commit e29cb51

Browse files
authored
Merge pull request #151 from unisoncomputing/cp/explicit-org-roles
Make org roles explicit in membership
2 parents 5ced64d + dbc30c4 commit e29cb51

33 files changed

+423
-628
lines changed

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: all clean install docker_server_build docker_push serve auth_example transcripts fixtures transcripts reset_fixtures
1+
.PHONY: all clean install docker_server_build docker_push serve auth_example transcripts fixtures transcripts serve_transcripts reset_fixtures
22

33
SHARE_PROJECT_ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
44
export SHARE_PROJECT_ROOT
@@ -81,6 +81,14 @@ reset_fixtures: $(installed_share)
8181
)
8282
@echo "Done!";
8383

84+
85+
serve_transcripts: $(installed_share)
86+
@echo "Taking down any existing docker dependencies"
87+
@docker compose -f docker/docker-compose.base.yml down || true
88+
@trap 'docker compose -f docker/docker-compose.base.yml down' EXIT INT TERM
89+
@echo "Booting up transcript docker dependencies..."
90+
docker compose -f docker/docker-compose.base.yml up --remove-orphans
91+
8492
transcripts: $(installed_share)
8593
@echo "Taking down any existing docker dependencies"
8694
@docker compose -f docker/docker-compose.base.yml down || true

share-client/src/Share/Client/Orgs.hs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
module Share.Client.Orgs
66
( createOrg,
7-
addOrgRoles,
8-
listOrgRoles,
9-
removeOrgRoles,
107
listOrgMembers,
118
addOrgMembers,
129
removeOrgMembers,
@@ -36,20 +33,9 @@ import Share.Web.Share.Orgs.Types
3633
orgsClient :: Client ClientM OrgsAPI
3734
orgsClient = client (Proxy :: Proxy OrgsAPI)
3835

39-
resourceRoutes :: UserHandle -> Client ClientM (NamedRoutes OrgsAPI.ResourceRoutes)
40-
orgRolesRoutes :: UserHandle -> Client ClientM (NamedRoutes OrgsAPI.OrgRolesRoutes)
41-
orgRolesRoutes = OrgsAPI.orgRoles <$> resourceRoutes
42-
4336
orgMembersRoutes :: UserHandle -> Client ClientM (NamedRoutes OrgsAPI.OrgMembersRoutes)
4437
orgMembersRoutes = OrgsAPI.orgMembers <$> resourceRoutes
4538

46-
listOrgRoles :: JWT.SignedJWT -> UserHandle -> ClientM ListRolesResponse
47-
listOrgRoles jwt userHandle = OrgsAPI.listOrgRoles (orgRolesRoutes userHandle) (jwtToAuthenticatedRequest jwt)
48-
49-
removeOrgRoles :: JWT.SignedJWT -> UserHandle -> OrgsAPI.RemoveRolesRequest -> ClientM ListRolesResponse
50-
removeOrgRoles jwt userHandle removeRolesReq =
51-
OrgsAPI.removeOrgRoles (orgRolesRoutes userHandle) (jwtToAuthenticatedRequest jwt) removeRolesReq
52-
5339
listOrgMembers :: JWT.SignedJWT -> UserHandle -> ClientM OrgMembersListResponse
5440
listOrgMembers jwt userHandle = OrgsAPI.listOrgMembers (orgMembersRoutes userHandle) (jwtToAuthenticatedRequest jwt)
5541

@@ -61,13 +47,10 @@ removeOrgMembers :: JWT.SignedJWT -> UserHandle -> OrgMembersRemoveRequest -> Cl
6147
removeOrgMembers jwt userHandle removeMembersReq =
6248
OrgsAPI.removeOrgMembers (orgMembersRoutes userHandle) (jwtToAuthenticatedRequest jwt) removeMembersReq
6349

64-
addOrgRoles :: JWT.SignedJWT -> UserHandle -> OrgsAPI.AddRolesRequest -> ClientM ListRolesResponse
65-
addOrgRoles jwt userHandle addRolesReq =
66-
OrgsAPI.addOrgRoles (orgRolesRoutes userHandle) (jwtToAuthenticatedRequest jwt) addRolesReq
67-
6850
createOrg :: JWT.SignedJWT -> CreateOrgRequest -> ClientM OrgDisplayInfo
6951
createOrg jwt createOrgReq =
7052
createOrg' (jwtToAuthenticatedRequest jwt) createOrgReq
7153

7254
createOrg' :: AuthenticatedRequest AuthenticatedUserId -> CreateOrgRequest -> ClientM OrgDisplayInfo
55+
resourceRoutes :: UserHandle -> OrgsAPI.ResourceRoutes (AsClientT ClientM)
7356
(createOrg' :<|> resourceRoutes) = orgsClient

sql/2025-04-08_public_resource_permissions.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ CREATE OR REPLACE VIEW user_resource_permissions(user_id, resource_id, permissio
3434
JOIN subject_resource_permissions srp
3535
ON sbu.subject_id = srp.subject_id
3636
UNION
37-
-- Include public resource permissions
37+
-- Include public resource permissions explicitly,
38+
-- since the above joins on subject_id which is NULL for public perms
3839
SELECT NULL, prp.resource_id, permission
3940
FROM public_resource_permissions prp
4041
);
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
-- Org membership is now associated with a specific role within the org, this simplifies things,
2+
-- makes the data more consistent, no need to rely on triggers, and makes it much easier to display in the UI.
3+
4+
5+
-- SANITY CHECK:
6+
-- SELECT org_user.handle, member_user.handle, role.ref
7+
-- FROM orgs org
8+
-- JOIN role_memberships rm ON rm.resource_id = org.resource_id
9+
-- JOIN users member_user ON rm.subject_id = member_user.subject_id
10+
-- JOIN roles role ON role.id = rm.role_id
11+
-- JOIN users org_user ON org.user_id = org_user.id;
12+
13+
-- SELECT org_user.handle, member_user.handle
14+
-- FROM org_members om
15+
-- JOIN users org_user ON om.organization_user_id = org_user.id
16+
-- JOIN users member_user ON om.member_user_id = member_user.id
17+
-- ;
18+
19+
20+
ALTER TABLE org_members
21+
ADD COLUMN role_id UUID REFERENCES roles(id) NULL;
22+
23+
-- set all existing org members to be maintiners
24+
UPDATE org_members
25+
SET role_id = (SELECT id FROM roles WHERE ref = 'org_maintainer' LIMIT 1);
26+
27+
-- Elevate the current org owners to have the org_owner role
28+
UPDATE org_members
29+
SET role_id = (SELECT id FROM roles WHERE ref = 'org_owner' LIMIT 1)
30+
WHERE EXISTS (
31+
SELECT
32+
FROM orgs org
33+
JOIN users u ON org_members.member_user_id = u.id
34+
JOIN role_memberships rm ON rm.subject_id = u.subject_id AND rm.resource_id = org.resource_id
35+
JOIN roles r ON rm.role_id = r.id
36+
WHERE org.id = org_members.org_id
37+
AND r.ref = 'org_owner'
38+
);
39+
40+
ALTER TABLE org_members
41+
ALTER COLUMN role_id SET NOT NULL;
42+
43+
-- Now add a check that each org always has an owner.
44+
CREATE OR REPLACE FUNCTION check_orgs_have_an_owner()
45+
RETURNS trigger AS $$
46+
BEGIN
47+
IF NOT EXISTS (
48+
SELECT
49+
FROM org_members om
50+
WHERE om.org_id = OLD.org_id
51+
AND om.role_id = (SELECT id FROM roles WHERE ref = 'org_owner' LIMIT 1)
52+
) THEN
53+
RAISE EXCEPTION 'Each organization must have at least one owner.';
54+
END IF;
55+
RETURN NULL;
56+
END;
57+
$$ LANGUAGE plpgsql;
58+
59+
CREATE TRIGGER check_org_owners_trigger
60+
AFTER UPDATE OR DELETE ON org_members
61+
FOR EACH ROW
62+
EXECUTE FUNCTION check_orgs_have_an_owner();
63+
64+
-- Split out view containing all the direct subject<->resource permissions.
65+
--
66+
-- This view expands the roles into their individual permissions
67+
-- but does not consider resource hierarchy or group memberships
68+
CREATE OR REPLACE VIEW direct_resource_permissions(subject_id, resource_id, permission) AS (
69+
SELECT rm.subject_id, rm.resource_id, permission
70+
FROM role_memberships rm
71+
JOIN roles r ON rm.role_id = r.id
72+
, UNNEST(r.permissions) AS permission
73+
UNION
74+
-- Include permissions from org membership roles
75+
SELECT u.subject_id, org.resource_id, permission
76+
FROM org_members om
77+
JOIN users u ON om.member_user_id = u.id
78+
JOIN roles r ON om.role_id = r.id
79+
JOIN orgs org ON om.org_id = org.id
80+
, UNNEST(r.permissions) AS permission
81+
UNION
82+
-- Include public resource permissions
83+
SELECT NULL, prp.resource_id, permission
84+
FROM public_resource_permissions prp
85+
);
86+
87+
88+
-- This view builds on top of direct_resource_permissions to include inherited permissions
89+
CREATE OR REPLACE VIEW subject_resource_permissions(subject_id, resource_id, permission) AS (
90+
SELECT drp.subject_id, drp.resource_id, drp.permission
91+
FROM direct_resource_permissions drp
92+
UNION
93+
-- Inherit permissions from parent resources
94+
SELECT drp.subject_id, rh.resource_id, drp.permission
95+
FROM direct_resource_permissions drp
96+
JOIN resource_hierarchy rh ON drp.resource_id = rh.parent_resource_id
97+
);
98+
99+
-- -- SANITY CHECK
100+
-- SELECT rm.subject_id, r.ref
101+
-- FROM role_memberships rm
102+
-- JOIN roles r ON rm.role_id = r.id
103+
-- -- JOIN users u ON rm.subject_id = u.subject_id
104+
-- WHERE
105+
-- r.ref::text IN ('org_viewer', 'org_maintainer', 'org_contributor', 'org_admin', 'org_owner', 'org_default');
106+
107+
DELETE FROM role_memberships rm
108+
USING roles r
109+
WHERE
110+
rm.role_id = r.id
111+
AND r.ref::text IN ('org_viewer', 'org_maintainer', 'org_contributor', 'org_admin', 'org_owner', 'org_default');
112+
113+
-- SANITY CHECK
114+
-- SELECT org_user.handle, member_user.handle, role.ref
115+
-- FROM org_members om
116+
-- JOIN users org_user ON om.organization_user_id = org_user.id
117+
-- JOIN users member_user ON om.member_user_id = member_user.id
118+
-- JOIN roles role ON role.id = om.role_id
119+
-- ;

src/Share/Postgres/Projects/Queries.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ isPremiumProject projId =
2727
SELECT EXISTS (SELECT FROM premium_projects WHERE project_id = #{projId})
2828
|]
2929

30-
listProjectRoles :: ProjectId -> Transaction e [(RoleAssignment ResolvedAuthSubject)]
30+
listProjectRoles :: ProjectId -> Transaction e [(RoleAssignment Set ResolvedAuthSubject)]
3131
listProjectRoles projId = do
3232
queryListRows @(ResolvedAuthSubject PG.:. Only ([RoleRef]))
3333
[sql|
@@ -42,7 +42,7 @@ listProjectRoles projId = do
4242
|]
4343
<&> fmap \(subject PG.:. Only roleRefs) -> (RoleAssignment {subject, roles = Set.fromList roleRefs})
4444

45-
addProjectRoles :: ProjectId -> [RoleAssignment ResolvedAuthSubject] -> Transaction e [(RoleAssignment ResolvedAuthSubject)]
45+
addProjectRoles :: ProjectId -> [RoleAssignment Set ResolvedAuthSubject] -> Transaction e [(RoleAssignment Set ResolvedAuthSubject)]
4646
addProjectRoles projId toAdd = do
4747
let addedRolesTable =
4848
toAdd
@@ -66,7 +66,7 @@ addProjectRoles projId toAdd = do
6666
|]
6767
listProjectRoles projId
6868

69-
removeProjectRoles :: ProjectId -> [RoleAssignment ResolvedAuthSubject] -> Transaction e [(RoleAssignment ResolvedAuthSubject)]
69+
removeProjectRoles :: ProjectId -> [RoleAssignment Set ResolvedAuthSubject] -> Transaction e [(RoleAssignment Set ResolvedAuthSubject)]
7070
removeProjectRoles projId toRemove = do
7171
let removedRolesTable =
7272
toRemove

src/Share/Prelude.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Share.Prelude
1818
altSum,
1919
altMap,
2020
foldMapM,
21+
foldMapMOf,
2122
onNothing,
2223
onNothingM,
2324
whenNothing,
@@ -56,6 +57,7 @@ where
5657
import Control.Applicative as X
5758
import Control.Arrow ((&&&))
5859
import Control.Category hiding (id, (.))
60+
import Control.Lens
5961
import Control.Monad as X
6062
import Control.Monad.Except (throwError)
6163
import Control.Monad.Reader as X
@@ -73,7 +75,6 @@ import Data.Foldable as X
7375
import Data.Function as X
7476
import Data.Functor as X
7577
import Data.Functor.Compose (Compose (..))
76-
import Data.Functor.Contravariant (contramap)
7778
import Data.Functor.Identity as X
7879
import Data.Int as X
7980
import Data.List.NonEmpty (NonEmpty (..))
@@ -219,3 +220,8 @@ unifyEither = either id id
219220

220221
traverseFirst :: (Bitraversable t, Applicative f) => (a -> f b) -> t a x -> f (t b x)
221222
traverseFirst f = bitraverse f pure
223+
224+
foldMapMOf :: (Monad m, Monoid w) => Fold s a -> (a -> m w) -> s -> m w
225+
foldMapMOf foc f s =
226+
toListOf foc s
227+
& foldMapM f

0 commit comments

Comments
 (0)