Skip to content

Commit da1cfdc

Browse files
authored
Merge pull request #52 from unisoncomputing/cp/org-role-constraints
Add additional constraints on org role/member management
2 parents bff609a + 8a9dfe8 commit da1cfdc

10 files changed

Lines changed: 103 additions & 6 deletions

File tree

src/Share/Web/Share/Orgs/Impl.hs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{-# LANGUAGE DataKinds #-}
2+
13
module Share.Web.Share.Orgs.Impl (server) where
24

35
import Control.Lens
@@ -10,6 +12,7 @@ import Share.Postgres qualified as PG
1012
import Share.Postgres.Users.Queries qualified as UserQ
1113
import Share.Prelude
1214
import Share.User (User (..))
15+
import Share.Utils.Logging qualified as Logging
1316
import Share.Web.App
1417
import Share.Web.Authorization qualified as AuthZ
1518
import Share.Web.Authorization.Types
@@ -23,6 +26,29 @@ import Share.Web.Share.Roles (canonicalRoleAssignmentOrdering)
2326
import Share.Web.Share.Roles.Queries (displaySubjectsOf)
2427
import Unison.Util.Set qualified as Set
2528

29+
data OrgError
30+
= OrgMemberOfOrgError
31+
| OrgMustHaveOwnerError
32+
deriving stock (Show, Eq)
33+
deriving (Logging.Loggable) via (Logging.ShowLoggable Logging.UserFault OrgError)
34+
35+
instance ToServerError OrgError where
36+
toServerError = \case
37+
OrgMemberOfOrgError ->
38+
( ErrorID "org:org-member-of-org",
39+
err400
40+
{ errBody = "Cannot add an org as a member of another org.",
41+
errReasonPhrase = "Invalid Org Member"
42+
}
43+
)
44+
OrgMustHaveOwnerError ->
45+
( ErrorID "org:must-have-owner",
46+
err400
47+
{ errBody = "Cannot remove the only owner of an org.",
48+
errReasonPhrase = "Invalid Org Member"
49+
}
50+
)
51+
2652
server :: ServerT API.API WebApp
2753
server =
2854
let orgResourceServer orgHandle =
@@ -75,6 +101,7 @@ addRolesEndpoint :: UserHandle -> UserId -> AddRolesRequest -> WebApp ListRolesR
75101
addRolesEndpoint orgHandle caller (AddRolesRequest {roleAssignments}) = do
76102
orgId <- orgIdByHandle orgHandle
77103
_authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkEditOrgRoles caller orgId
104+
assertNoOrgSubjects roleAssignments
78105
PG.runTransaction do
79106
orgRoles <- OrgQ.addOrgRoles orgId roleAssignments
80107
ListRolesResponse True . canonicalRoleAssignmentOrdering <$> displaySubjectsOf (traversed . traversed) orgRoles
@@ -83,8 +110,12 @@ removeRolesEndpoint :: UserHandle -> UserId -> RemoveRolesRequest -> WebApp List
83110
removeRolesEndpoint orgHandle caller (RemoveRolesRequest {roleAssignments}) = do
84111
orgId <- orgIdByHandle orgHandle
85112
_authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkEditOrgRoles caller orgId
86-
PG.runTransaction do
113+
PG.runTransactionOrRespondError do
87114
orgRoles <- OrgQ.removeOrgRoles orgId roleAssignments
115+
OrgQ.doesOrgHaveOwner orgId >>= \case
116+
False -> throwError OrgMustHaveOwnerError
117+
True -> pure ()
118+
88119
ListRolesResponse True . canonicalRoleAssignmentOrdering <$> displaySubjectsOf (traversed . traversed) orgRoles
89120

90121
listMembersEndpoint :: UserHandle -> UserId -> WebApp OrgMembersListResponse
@@ -98,8 +129,12 @@ addMembersEndpoint :: UserHandle -> UserId -> OrgMembersAddRequest -> WebApp Org
98129
addMembersEndpoint orgHandle caller (OrgMembersAddRequest {members}) = do
99130
orgId <- orgIdByHandle orgHandle
100131
_authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkEditOrgMembers caller orgId
101-
PG.runTransaction do
132+
PG.runTransactionOrRespondError do
102133
userIds <- UserQ.userIdsByHandlesOf Set.traverse (Set.fromList members)
134+
hasOrgMember <- runMaybeT $ for_ userIds \userId -> do
135+
MaybeT $ OrgQ.orgByUserId userId
136+
when (isJust hasOrgMember) do
137+
throwError OrgMemberOfOrgError
103138
OrgQ.addOrgMembers orgId userIds
104139
OrgMembersListResponse <$> OrgQ.listOrgMembers orgId
105140

@@ -111,3 +146,15 @@ removeMembersEndpoint orgHandle caller (OrgMembersRemoveRequest {members}) = do
111146
userIds <- UserQ.userIdsByHandlesOf Set.traverse (Set.fromList members)
112147
OrgQ.removeOrgMembers orgId userIds
113148
OrgMembersListResponse <$> OrgQ.listOrgMembers orgId
149+
150+
assertNoOrgSubjects :: [RoleAssignment ResolvedAuthSubject] -> WebApp ()
151+
assertNoOrgSubjects roleAssignments = do
152+
let hasOrgSubject =
153+
roleAssignments
154+
& any
155+
( \RoleAssignment {subject} -> case subject of
156+
OrgSubject {} -> True
157+
_ -> False
158+
)
159+
when hasOrgSubject do
160+
respondError OrgMemberOfOrgError

src/Share/Web/Share/Orgs/Queries.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Share.Web.Share.Orgs.Queries
1111
removeOrgMembers,
1212
orgDisplayInfoOf,
1313
userDisplayInfoByOrgIdOf,
14+
doesOrgHaveOwner,
1415
)
1516
where
1617

@@ -178,3 +179,17 @@ removeOrgMembers orgId toRemove = do
178179
WHERE org_members.member_user_id = v.member_user_id
179180
AND org_members.org_id = #{orgId}
180181
|]
182+
183+
doesOrgHaveOwner :: OrgId -> Transaction e Bool
184+
doesOrgHaveOwner orgId = do
185+
queryExpect1Col
186+
[sql|
187+
SELECT EXISTS (
188+
SELECT
189+
FROM role_memberships rm
190+
JOIN roles role ON rm.role_id = role.id
191+
JOIN orgs org ON rm.resource_id = org.resource_id
192+
WHERE org.id = #{orgId}
193+
AND role.ref = 'org_owner'::role_ref
194+
)
195+
|]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"body": "Cannot add an org as a member of another org.",
3+
"status": [
4+
{
5+
"status_code": 400
6+
}
7+
]
8+
}

transcripts/share-apis/orgs/run.zsh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ fetch "$transcript_user" POST org-add-members '/orgs/acme/members' '{
5656
]
5757
}'
5858

59+
# Can't add an org to another org
60+
fetch "$transcript_user" POST org-cant-have-org-members '/orgs/acme/members' '{
61+
"members": [
62+
"unison"
63+
]
64+
}'
65+
5966
fetch "$transcript_user" GET org-get-members-after-adding '/orgs/acme/members'
6067

6168
# Owner can remove members
@@ -65,4 +72,6 @@ fetch "$transcript_user" DELETE org-remove-members '/orgs/acme/members' '{
6572
]
6673
}'
6774

75+
6876
fetch "$transcript_user" GET org-get-members-after-removing '/orgs/acme/members'
77+

transcripts/share-apis/roles/grant-project-contributor.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
{
2020
"roles": [
21-
"org_admin"
21+
"org_owner"
2222
],
2323
"subject": {
2424
"data": {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"body": "Cannot remove the only owner of an org.",
3+
"status": [
4+
{
5+
"status_code": 400
6+
}
7+
]
8+
}

transcripts/share-apis/roles/org-roles-list.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"role_assignments": [
55
{
66
"roles": [
7-
"org_admin"
7+
"org_owner"
88
],
99
"subject": {
1010
"data": {

transcripts/share-apis/roles/revoke-project-contributor.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"role_assignments": [
55
{
66
"roles": [
7-
"org_admin"
7+
"org_owner"
88
],
99
"subject": {
1010
"data": {

transcripts/share-apis/roles/run.zsh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ fetch "$some_user" GET non-maintainer-project-view '/users/unison/projects/priva
1515

1616
fetch "$test_user" GET org-roles-list '/orgs/unison/roles'
1717

18+
# Org must always have an owner
19+
fetch "$test_user" DELETE org-remove-only-owner '/orgs/unison/roles' "
20+
{
21+
\"role_assignments\":
22+
[ { \"subject\": {\"kind\": \"user\", \"id\": \"${test_user}\"}
23+
, \"roles\": [\"org_owner\"]
24+
}
25+
]
26+
}"
27+
1828
# Giving this user the project_contributor role on the Unison org should give them access to the project owned by that org.
1929
# Typically you'd add the user to the org, but this is a good way to test JUST the resource role hierarchy.
2030
fetch "$test_user" POST grant-project-contributor '/orgs/unison/roles' "

transcripts/sql/inserts.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ VALUES (
8383
INSERT INTO role_memberships(subject_id, resource_id, role_id)
8484
SELECT (SELECT u.subject_id FROM users u WHERE u.handle = 'test'),
8585
(SELECT org.resource_id FROM orgs org JOIN users orgu ON org.user_id = orgu.id WHERE orgu.handle = 'unison'),
86-
(SELECT r.id FROM roles r WHERE r.ref = 'org_admin');
86+
(SELECT r.id FROM roles r WHERE r.ref = 'org_owner');
8787

8888
INSERT INTO tours (
8989
user_id,

0 commit comments

Comments
 (0)