Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
105 changes: 105 additions & 0 deletions sql/2025-09-22_org_membership_roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
-- 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.

-- Update the role permissions, some roles had an incorrect 'org:create_project' rather than 'project:create' permission
-- UPDATE roles
-- SET permissions = ARRAY['org:view', 'org:manage', 'org:admin', 'team:view', 'team:manage', 'project:view', 'project:create', 'project:manage', 'project:contribute', 'project:delete', 'project:maintain']
-- WHERE ref = 'org_admin'
-- ;

-- UPDATE roles
-- SET permissions = ARRAY['org:view', 'org:manage', 'org:admin', 'org:delete', 'org:change_owner', 'team:view', 'team:manage', 'project:view', 'project:create', 'project:manage', 'project:contribute', 'project:delete', 'project:maintain']
-- WHERE ref = 'org_owner'
-- ;

-- -- After migrating, this will no longer be used.
-- UPDATE roles
-- SET permissions = ARRAY['org:view', 'org:edit', 'team:view', 'project:view', 'project:create', 'project:maintain', 'project:contribute']
-- WHERE ref = 'org_default'
-- ;

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
);


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');
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