Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d2ea1ac
Add explicit role to org_members
ChrisPenner Sep 22, 2025
f964370
Remove Org Roles endpoints
ChrisPenner Sep 22, 2025
f9637af
Generalize RoleAssignment type over its container
ChrisPenner Sep 22, 2025
117b47a
Update listOrgMembers query
ChrisPenner Sep 22, 2025
7543704
Add foldMapMOf to prelude
ChrisPenner Sep 22, 2025
47f2b46
Fix org members queries
ChrisPenner Sep 22, 2025
c60b248
Add trigger to enforce every org has an owner
ChrisPenner Sep 22, 2025
0e4c299
Check that there's still an howner when removing org members
ChrisPenner Sep 22, 2025
dbb9df3
Remove org roles routes from share client
ChrisPenner Sep 22, 2025
f854a7e
Remove unused roles transcripts
ChrisPenner Sep 22, 2025
745ab94
Add target for running transcript docker deps
ChrisPenner Sep 22, 2025
d2167ab
Fix up migration sql syntax
ChrisPenner Sep 22, 2025
acaf23c
Fix org members in inserts.sql
ChrisPenner Sep 22, 2025
ff27eaa
Fix bad column ref in new validation trigger
ChrisPenner Sep 22, 2025
ad2f4e0
Fix up role permissions for project creation
ChrisPenner Sep 22, 2025
045aab3
Fix super subtle permissions typo
ChrisPenner Sep 22, 2025
e767697
Rerun org transcripts
ChrisPenner Sep 22, 2025
4dfbc7f
Fix up naming of transcript artifacts
ChrisPenner Sep 22, 2025
a7ec3dd
Delete old unused role memberships
ChrisPenner Sep 22, 2025
063399f
Use correct 'role' or 'roles' delineation in JSON
ChrisPenner Sep 22, 2025
ff3dc09
roles -> role
ChrisPenner Sep 22, 2025
bf470ca
Uncomment role updates
ChrisPenner Oct 14, 2025
a95ccac
Remove role transcripts
ChrisPenner Oct 14, 2025
788aacf
Remove incorrect role rewrites
ChrisPenner Oct 14, 2025
dbc30c4
Add sanity checks to migrations
ChrisPenner Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all clean install docker_server_build docker_push serve auth_example transcripts fixtures transcripts reset_fixtures
.PHONY: all clean install docker_server_build docker_push serve auth_example transcripts fixtures transcripts serve_transcripts reset_fixtures

SHARE_PROJECT_ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
export SHARE_PROJECT_ROOT
Expand Down Expand Up @@ -81,6 +81,14 @@ reset_fixtures: $(installed_share)
)
@echo "Done!";


serve_transcripts: $(installed_share)
@echo "Taking down any existing docker dependencies"
@docker compose -f docker/docker-compose.base.yml down || true
@trap 'docker compose -f docker/docker-compose.base.yml down' EXIT INT TERM
@echo "Booting up transcript docker dependencies..."
docker compose -f docker/docker-compose.base.yml up --remove-orphans

transcripts: $(installed_share)
@echo "Taking down any existing docker dependencies"
@docker compose -f docker/docker-compose.base.yml down || true
Expand Down
19 changes: 1 addition & 18 deletions share-client/src/Share/Client/Orgs.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

module Share.Client.Orgs
( createOrg,
addOrgRoles,
listOrgRoles,
removeOrgRoles,
listOrgMembers,
addOrgMembers,
removeOrgMembers,
Expand Down Expand Up @@ -36,20 +33,9 @@ import Share.Web.Share.Orgs.Types
orgsClient :: Client ClientM OrgsAPI
orgsClient = client (Proxy :: Proxy OrgsAPI)

resourceRoutes :: UserHandle -> Client ClientM (NamedRoutes OrgsAPI.ResourceRoutes)
orgRolesRoutes :: UserHandle -> Client ClientM (NamedRoutes OrgsAPI.OrgRolesRoutes)
orgRolesRoutes = OrgsAPI.orgRoles <$> resourceRoutes

orgMembersRoutes :: UserHandle -> Client ClientM (NamedRoutes OrgsAPI.OrgMembersRoutes)
orgMembersRoutes = OrgsAPI.orgMembers <$> resourceRoutes

listOrgRoles :: JWT.SignedJWT -> UserHandle -> ClientM ListRolesResponse
listOrgRoles jwt userHandle = OrgsAPI.listOrgRoles (orgRolesRoutes userHandle) (jwtToAuthenticatedRequest jwt)

removeOrgRoles :: JWT.SignedJWT -> UserHandle -> OrgsAPI.RemoveRolesRequest -> ClientM ListRolesResponse
removeOrgRoles jwt userHandle removeRolesReq =
OrgsAPI.removeOrgRoles (orgRolesRoutes userHandle) (jwtToAuthenticatedRequest jwt) removeRolesReq

listOrgMembers :: JWT.SignedJWT -> UserHandle -> ClientM OrgMembersListResponse
listOrgMembers jwt userHandle = OrgsAPI.listOrgMembers (orgMembersRoutes userHandle) (jwtToAuthenticatedRequest jwt)

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

addOrgRoles :: JWT.SignedJWT -> UserHandle -> OrgsAPI.AddRolesRequest -> ClientM ListRolesResponse
addOrgRoles jwt userHandle addRolesReq =
OrgsAPI.addOrgRoles (orgRolesRoutes userHandle) (jwtToAuthenticatedRequest jwt) addRolesReq

createOrg :: JWT.SignedJWT -> CreateOrgRequest -> ClientM OrgDisplayInfo
createOrg jwt createOrgReq =
createOrg' (jwtToAuthenticatedRequest jwt) createOrgReq

createOrg' :: AuthenticatedRequest AuthenticatedUserId -> CreateOrgRequest -> ClientM OrgDisplayInfo
resourceRoutes :: UserHandle -> OrgsAPI.ResourceRoutes (AsClientT ClientM)
(createOrg' :<|> resourceRoutes) = orgsClient
3 changes: 2 additions & 1 deletion sql/2025-04-08_public_resource_permissions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ CREATE OR REPLACE VIEW user_resource_permissions(user_id, resource_id, permissio
JOIN subject_resource_permissions srp
ON sbu.subject_id = srp.subject_id
UNION
-- Include public resource permissions
-- Include public resource permissions explicitly,
-- since the above joins on subject_id which is NULL for public perms
SELECT NULL, prp.resource_id, permission
FROM public_resource_permissions prp
);
Expand Down
119 changes: 119 additions & 0 deletions sql/2025-09-22_org_membership_roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
-- Org membership is now associated with a specific role within the org, this simplifies things,
-- makes the data more consistent, no need to rely on triggers, and makes it much easier to display in the UI.


-- SANITY CHECK:
-- SELECT org_user.handle, member_user.handle, role.ref
-- FROM orgs org
-- JOIN role_memberships rm ON rm.resource_id = org.resource_id
-- JOIN users member_user ON rm.subject_id = member_user.subject_id
-- JOIN roles role ON role.id = rm.role_id
-- JOIN users org_user ON org.user_id = org_user.id;

-- SELECT org_user.handle, member_user.handle
-- FROM org_members om
-- JOIN users org_user ON om.organization_user_id = org_user.id
-- JOIN users member_user ON om.member_user_id = member_user.id
-- ;


ALTER TABLE org_members
ADD COLUMN role_id UUID REFERENCES roles(id) NULL;

-- set all existing org members to be maintiners
UPDATE org_members
SET role_id = (SELECT id FROM roles WHERE ref = 'org_maintainer' LIMIT 1);

-- Elevate the current org owners to have the org_owner role
UPDATE org_members
SET role_id = (SELECT id FROM roles WHERE ref = 'org_owner' LIMIT 1)
WHERE EXISTS (
SELECT
FROM orgs org
JOIN users u ON org_members.member_user_id = u.id
JOIN role_memberships rm ON rm.subject_id = u.subject_id AND rm.resource_id = org.resource_id
JOIN roles r ON rm.role_id = r.id
WHERE org.id = org_members.org_id
AND r.ref = 'org_owner'
);

ALTER TABLE org_members
ALTER COLUMN role_id SET NOT NULL;

-- Now add a check that each org always has an owner.
CREATE OR REPLACE FUNCTION check_orgs_have_an_owner()
RETURNS trigger AS $$
BEGIN
IF NOT EXISTS (
SELECT
FROM org_members om
WHERE om.org_id = OLD.org_id
AND om.role_id = (SELECT id FROM roles WHERE ref = 'org_owner' LIMIT 1)
) THEN
RAISE EXCEPTION 'Each organization must have at least one owner.';
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER check_org_owners_trigger
AFTER UPDATE OR DELETE ON org_members
FOR EACH ROW
EXECUTE FUNCTION check_orgs_have_an_owner();

-- Split out view containing all the direct subject<->resource permissions.
--
-- This view expands the roles into their individual permissions
-- but does not consider resource hierarchy or group memberships
CREATE OR REPLACE VIEW direct_resource_permissions(subject_id, resource_id, permission) AS (
SELECT rm.subject_id, rm.resource_id, permission
FROM role_memberships rm
JOIN roles r ON rm.role_id = r.id
, UNNEST(r.permissions) AS permission
UNION
-- Include permissions from org membership roles
SELECT u.subject_id, org.resource_id, permission
FROM org_members om
JOIN users u ON om.member_user_id = u.id
JOIN roles r ON om.role_id = r.id
JOIN orgs org ON om.org_id = org.id
, UNNEST(r.permissions) AS permission
UNION
-- Include public resource permissions
SELECT NULL, prp.resource_id, permission
FROM public_resource_permissions prp
);


-- This view builds on top of direct_resource_permissions to include inherited permissions
CREATE OR REPLACE VIEW subject_resource_permissions(subject_id, resource_id, permission) AS (
SELECT drp.subject_id, drp.resource_id, drp.permission
FROM direct_resource_permissions drp
UNION
-- Inherit permissions from parent resources
SELECT drp.subject_id, rh.resource_id, drp.permission
FROM direct_resource_permissions drp
JOIN resource_hierarchy rh ON drp.resource_id = rh.parent_resource_id
);

-- -- SANITY CHECK
-- SELECT rm.subject_id, r.ref
-- FROM role_memberships rm
-- JOIN roles r ON rm.role_id = r.id
-- -- JOIN users u ON rm.subject_id = u.subject_id
-- WHERE
-- r.ref::text IN ('org_viewer', 'org_maintainer', 'org_contributor', 'org_admin', 'org_owner', 'org_default');

DELETE FROM role_memberships rm
USING roles r
WHERE
rm.role_id = r.id
AND r.ref::text IN ('org_viewer', 'org_maintainer', 'org_contributor', 'org_admin', 'org_owner', 'org_default');

-- SANITY CHECK
-- SELECT org_user.handle, member_user.handle, role.ref
-- FROM org_members om
-- JOIN users org_user ON om.organization_user_id = org_user.id
-- JOIN users member_user ON om.member_user_id = member_user.id
-- JOIN roles role ON role.id = om.role_id
-- ;
6 changes: 3 additions & 3 deletions src/Share/Postgres/Projects/Queries.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ isPremiumProject projId =
SELECT EXISTS (SELECT FROM premium_projects WHERE project_id = #{projId})
|]

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

addProjectRoles :: ProjectId -> [RoleAssignment ResolvedAuthSubject] -> Transaction e [(RoleAssignment ResolvedAuthSubject)]
addProjectRoles :: ProjectId -> [RoleAssignment Set ResolvedAuthSubject] -> Transaction e [(RoleAssignment Set ResolvedAuthSubject)]
addProjectRoles projId toAdd = do
let addedRolesTable =
toAdd
Expand All @@ -66,7 +66,7 @@ addProjectRoles projId toAdd = do
|]
listProjectRoles projId

removeProjectRoles :: ProjectId -> [RoleAssignment ResolvedAuthSubject] -> Transaction e [(RoleAssignment ResolvedAuthSubject)]
removeProjectRoles :: ProjectId -> [RoleAssignment Set ResolvedAuthSubject] -> Transaction e [(RoleAssignment Set ResolvedAuthSubject)]
removeProjectRoles projId toRemove = do
let removedRolesTable =
toRemove
Expand Down
8 changes: 7 additions & 1 deletion src/Share/Prelude.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module Share.Prelude
altSum,
altMap,
foldMapM,
foldMapMOf,
onNothing,
onNothingM,
whenNothing,
Expand Down Expand Up @@ -56,6 +57,7 @@ where
import Control.Applicative as X
import Control.Arrow ((&&&))
import Control.Category hiding (id, (.))
import Control.Lens
import Control.Monad as X
import Control.Monad.Except (throwError)
import Control.Monad.Reader as X
Expand All @@ -73,7 +75,6 @@ import Data.Foldable as X
import Data.Function as X
import Data.Functor as X
import Data.Functor.Compose (Compose (..))
import Data.Functor.Contravariant (contramap)
import Data.Functor.Identity as X
import Data.Int as X
import Data.List.NonEmpty (NonEmpty (..))
Expand Down Expand Up @@ -219,3 +220,8 @@ unifyEither = either id id

traverseFirst :: (Bitraversable t, Applicative f) => (a -> f b) -> t a x -> f (t b x)
traverseFirst f = bitraverse f pure

foldMapMOf :: (Monad m, Monoid w) => Fold s a -> (a -> m w) -> s -> m w
foldMapMOf foc f s =
toListOf foc s
& foldMapM f
Loading
Loading