Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 55 additions & 22 deletions packages/client/src/components/SideBar.component.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { FC, ReactNode, useState } from 'react';
import { FC, ReactNode, useEffect, useState } from 'react';
import { Collapse, Divider, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { ExpandMore, ExpandLess, School, Dataset, Work, Logout, GroupWork } from '@mui/icons-material';
import { useAuth } from '../context/Auth.context';
import { useNavigate } from 'react-router-dom';
import { Environment } from './Environment.component';
import { Permission } from '../graphql/graphql';
import { useGetRolesQuery } from '../graphql/permission/permission';
import { useProject } from '../context/Project.context';
import { useStudy } from '../context/Study.context';

interface SideBarProps {
open: boolean;
Expand All @@ -13,49 +17,70 @@ interface SideBarProps {
export const SideBar: FC<SideBarProps> = ({ open, drawerWidth }) => {
const { logout } = useAuth();
const navigate = useNavigate();
const [permission, setPermission] = useState<Permission | null>(null);
const { project } = useProject();
const { study } = useStudy();
const rolesQueryResults = useGetRolesQuery({ variables: { project: project?._id, study: study?._id } });

useEffect(() => {
if (rolesQueryResults.data) {
setPermission(rolesQueryResults.data.getRoles);
}
}, [rolesQueryResults.data]);

const navItems: NavItemProps[] = [
{
name: 'Projects',
icon: <Work />,
action: () => {},
visible: (p) => p!.owner || p!.projectAdmin,
permission,
subItems: [
{ name: 'New Project', action: () => navigate('/project/new') },
{ name: 'Project Control', action: () => navigate('/project/controls') },
{ name: 'User Permissions', action: () => navigate('/project/permissions') }
{ name: 'New Project', action: () => navigate('/project/new'), visible: (p) => p!.owner },
{ name: 'Project Control', action: () => navigate('/project/controls'), visible: (p) => p!.owner },
{ name: 'User Permissions', action: () => navigate('/project/permissions'), visible: (p) => p!.projectAdmin }
]
},
{
name: 'Studies',
action: () => {},
icon: <School />,
visible: (p) => p!.projectAdmin || p!.studyAdmin,
permission,
subItems: [
{ name: 'New Study', action: () => navigate('/study/new') },
{ name: 'Study Control', action: () => navigate('/study/controls') },
{ name: 'User Permissions', action: () => navigate('/study/permissions') },
{ name: 'Entry Controls', action: () => navigate('/study/controls') },
{ name: 'Download Tags', action: () => navigate('/study/tags') }
{ name: 'New Study', action: () => navigate('/study/new'), visible: (p) => p!.projectAdmin },
{ name: 'Study Control', action: () => navigate('/study/controls'), visible: (p) => p!.projectAdmin },
{ name: 'User Permissions', action: () => navigate('/study/permissions'), visible: (p) => p!.studyAdmin },
{ name: 'Entry Controls', action: () => navigate('/study/controls'), visible: (p) => p!.studyAdmin },
{ name: 'Download Tags', action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin }
]
},
{
name: 'Datasets',
action: () => {},
icon: <Dataset />,
visible: (p) => p!.owner,
permission,
subItems: [
{ name: 'Dataset Control', action: () => navigate('/dataset/controls') },
{ name: 'Project Access', action: () => navigate('/dataset/projectaccess') }
{ name: 'Dataset Control', action: () => navigate('/dataset/controls'), visible: (p) => p!.owner },
{ name: 'Project Access', action: () => navigate('/dataset/projectaccess'), visible: (p) => p!.owner }
]
},
{
name: 'Contribute',
action: () => {},
icon: <GroupWork />,
subItems: [{ name: 'Tag in Study', action: () => navigate('/contribute/landing') }]
permission,
visible: (p) => p!.contributor,
subItems: [
{ name: 'Tag in Study', action: () => navigate('/contribute/landing'), visible: (p) => p!.contributor }
]
},
{
name: 'Logout',
action: logout,
icon: <Logout />
icon: <Logout />,
visible: () => true
}
];

Expand All @@ -77,11 +102,15 @@ export const SideBar: FC<SideBarProps> = ({ open, drawerWidth }) => {
anchor="left"
open={open}
>
<List sx={{ paddingTop: '30px' }}>
{navItems.map((navItem) => (
<NavItem {...navItem} key={navItem.name} />
))}
</List>
{permission && (
<List sx={{ paddingTop: '30px' }}>
{navItems
.filter((navItem) => navItem.visible(permission))
.map((navItem) => (
<NavItem {...navItem} key={navItem.name} />
))}
</List>
)}
<Environment />
</Drawer>
);
Expand All @@ -92,9 +121,11 @@ interface NavItemProps {
name: string;
icon?: ReactNode;
subItems?: NavItemProps[];
permission?: Permission | null;
visible: (permission: Permission | null | undefined) => boolean;
}

const NavItem: FC<NavItemProps> = ({ action, name, icon, subItems }) => {
const NavItem: FC<NavItemProps> = ({ action, name, icon, subItems, permission }) => {
const isExpandable = subItems && subItems.length > 0;
const [open, setOpen] = useState(false);

Expand All @@ -107,9 +138,11 @@ const NavItem: FC<NavItemProps> = ({ action, name, icon, subItems }) => {
<Collapse in={open} timeout="auto" unmountOnExit>
<Divider />
<List disablePadding>
{subItems.map((item, index) => (
<NavItem {...item} key={index} />
))}
{subItems
.filter((subItem) => subItem.visible(permission))
.map((item, index) => (
<NavItem {...item} key={index} />
))}
</List>
</Collapse>
) : null;
Expand Down
16 changes: 16 additions & 0 deletions packages/client/src/graphql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,15 @@ export type OrganizationCreate = {
projectId: Scalars['String']['input'];
};

export type Permission = {
__typename?: 'Permission';
contributor: Scalars['Boolean']['output'];
owner: Scalars['Boolean']['output'];
projectAdmin: Scalars['Boolean']['output'];
studyAdmin: Scalars['Boolean']['output'];
trainedContributor: Scalars['Boolean']['output'];
};

export type Project = {
__typename?: 'Project';
_id: Scalars['ID']['output'];
Expand Down Expand Up @@ -548,6 +557,7 @@ export type Query = {
getProject: ProjectModel;
getProjectPermissions: Array<ProjectPermissionModel>;
getProjects: Array<Project>;
getRoles: Permission;
getStudyPermissions: Array<StudyPermissionModel>;
getUser: UserModel;
invite: InviteModel;
Expand Down Expand Up @@ -613,6 +623,12 @@ export type QueryGetProjectPermissionsArgs = {
};


export type QueryGetRolesArgs = {
project?: InputMaybe<Scalars['ID']['input']>;
study?: InputMaybe<Scalars['ID']['input']>;
};


export type QueryGetStudyPermissionsArgs = {
study: Scalars['ID']['input'];
};
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/graphql/permission/permission.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,13 @@ query getDatasetProjectPermissions($project: ID!) {
mutation grantProjectDatasetAccess($project: ID!, $dataset: ID!, $hasAccess: Boolean!) {
grantProjectDatasetAccess(project: $project, dataset: $dataset, hasAccess: $hasAccess)
}

query getRoles($project: ID, $study: ID) {
getRoles(project: $project, study: $study) {
owner,
projectAdmin,
studyAdmin,
trainedContributor,
contributor
}
}
50 changes: 49 additions & 1 deletion packages/client/src/graphql/permission/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export type GrantProjectDatasetAccessMutationVariables = Types.Exact<{

export type GrantProjectDatasetAccessMutation = { __typename?: 'Mutation', grantProjectDatasetAccess: boolean };

export type GetRolesQueryVariables = Types.Exact<{
project?: Types.InputMaybe<Types.Scalars['ID']['input']>;
study?: Types.InputMaybe<Types.Scalars['ID']['input']>;
}>;


export type GetRolesQuery = { __typename?: 'Query', getRoles: { __typename?: 'Permission', owner: boolean, projectAdmin: boolean, studyAdmin: boolean, trainedContributor: boolean, contributor: boolean } };


export const GetProjectPermissionsDocument = gql`
query getProjectPermissions($project: ID!) {
Expand Down Expand Up @@ -378,4 +386,44 @@ export function useGrantProjectDatasetAccessMutation(baseOptions?: Apollo.Mutati
}
export type GrantProjectDatasetAccessMutationHookResult = ReturnType<typeof useGrantProjectDatasetAccessMutation>;
export type GrantProjectDatasetAccessMutationResult = Apollo.MutationResult<GrantProjectDatasetAccessMutation>;
export type GrantProjectDatasetAccessMutationOptions = Apollo.BaseMutationOptions<GrantProjectDatasetAccessMutation, GrantProjectDatasetAccessMutationVariables>;
export type GrantProjectDatasetAccessMutationOptions = Apollo.BaseMutationOptions<GrantProjectDatasetAccessMutation, GrantProjectDatasetAccessMutationVariables>;
export const GetRolesDocument = gql`
query getRoles($project: ID, $study: ID) {
getRoles(project: $project, study: $study) {
owner
projectAdmin
studyAdmin
trainedContributor
contributor
}
}
`;

/**
* __useGetRolesQuery__
*
* To run a query within a React component, call `useGetRolesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRolesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetRolesQuery({
* variables: {
* project: // value for 'project'
* study: // value for 'study'
* },
* });
*/
export function useGetRolesQuery(baseOptions?: Apollo.QueryHookOptions<GetRolesQuery, GetRolesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetRolesQuery, GetRolesQueryVariables>(GetRolesDocument, options);
}
export function useGetRolesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRolesQuery, GetRolesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetRolesQuery, GetRolesQueryVariables>(GetRolesDocument, options);
}
export type GetRolesQueryHookResult = ReturnType<typeof useGetRolesQuery>;
export type GetRolesLazyQueryHookResult = ReturnType<typeof useGetRolesLazyQuery>;
export type GetRolesQueryResult = Apollo.QueryResult<GetRolesQuery, GetRolesQueryVariables>;
9 changes: 9 additions & 0 deletions packages/server/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ type DatasetProjectPermission {
projectHasAccess: Boolean!
}

type Permission {
owner: Boolean!
projectAdmin: Boolean!
studyAdmin: Boolean!
trainedContributor: Boolean!
contributor: Boolean!
}

type Entry {
_id: String!
organization: ID!
Expand Down Expand Up @@ -137,6 +145,7 @@ type Query {
getProjectPermissions(project: ID!): [ProjectPermissionModel!]!
getStudyPermissions(study: ID!): [StudyPermissionModel!]!
getDatasetProjectPermissions(project: ID!): [DatasetProjectPermission!]!
getRoles(project: ID, study: ID): Permission!
projectExists(name: String!): Boolean!
getProjects: [Project!]!
studyExists(name: String!, project: ID!): Boolean!
Expand Down
19 changes: 19 additions & 0 deletions packages/server/src/permission/models/permission.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ObjectType, Field } from '@nestjs/graphql';

@ObjectType()
export class Permission {
@Field()
owner: boolean;

@Field()
projectAdmin: boolean;

@Field()
studyAdmin: boolean;

@Field()
trainedContributor: boolean;

@Field()
contributor: boolean;
}
4 changes: 3 additions & 1 deletion packages/server/src/permission/permission.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { OwnerPermissionResolver } from './resolvers/owner.resolver';
import { StudyPermissionResolver } from './resolvers/study.resolver';
import { DatasetPermissionResolver } from './resolvers/dataset.resolver';
import { DatasetModule } from '../dataset/dataset.module';
import { PermissionResolver } from './resolvers/permission.resolver';

@Module({
imports: [
Expand All @@ -23,7 +24,8 @@ import { DatasetModule } from '../dataset/dataset.module';
ProjectPermissionResolver,
OwnerPermissionResolver,
StudyPermissionResolver,
DatasetPermissionResolver
DatasetPermissionResolver,
PermissionResolver
],
exports: [casbinProvider]
})
Expand Down
19 changes: 19 additions & 0 deletions packages/server/src/permission/permission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { StudyPermissionModel } from './models/study.model';
import { Dataset } from '../dataset/dataset.model';
import { DatasetService } from '../dataset/dataset.service';
import { DatasetProjectPermission } from './models/dataset.model';
import { Permission } from './models/permission.model';
import { Organization } from '../organization/organization.model';

@Injectable()
export class PermissionService {
Expand Down Expand Up @@ -195,4 +197,21 @@ export class PermissionService {
})
);
}

async getRoles(
user: TokenPayload,
organization: Organization,
project: Project | null,
study: Study | null
): Promise<Permission> {
return {
owner: await this.enforcer.enforce(user.id, Roles.OWNER, organization._id.toString()),
projectAdmin: project ? await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, project._id.toString()) : false,
studyAdmin: study ? await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString()) : false,
trainedContributor: study
? await this.enforcer.enforce(user.id, Roles.TRAINED_CONTRIBUTOR, study._id.toString())
: false,
contributor: study ? await this.enforcer.enforce(user.id, Roles.CONTRIBUTOR, study._id.toString()) : false
};
}
}
43 changes: 43 additions & 0 deletions packages/server/src/permission/resolvers/permission.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Resolver, Query, Args, ID } from '@nestjs/graphql';
import { StudyPipe } from '../../study/pipes/study.pipe';
import { ProjectPipe } from '../../project/pipes/project.pipe';
import { TokenContext } from '../../jwt/token.context';
import { Permission } from '../models/permission.model';
import { TokenPayload } from '../../jwt/token.dto';
import { Study } from '../../study/study.model';
import { Project } from '../../project/project.model';
import { PermissionService } from '../permission.service';
import { OrganizationContext } from '../../organization/organization.context';
import { Organization } from '../../organization/organization.model';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../jwt/jwt.guard';

@UseGuards(JwtAuthGuard)
@Resolver()
export class PermissionResolver {
constructor(
private readonly studyPipe: StudyPipe,
private readonly projectPipe: ProjectPipe,
private readonly permissionService: PermissionService
) {}

@Query(() => Permission)
async getRoles(
@Args('project', { type: () => ID, nullable: true }) projectID: string | null,
@Args('study', { type: () => ID, nullable: true }) studyID: string | null,
@TokenContext() tokenContext: TokenPayload,
@OrganizationContext() organization: Organization
): Promise<Permission> {
let project: Project | null = null;
let study: Study | null = null;

if (projectID) {
project = await this.projectPipe.transform(projectID);
}
if (studyID) {
study = await this.studyPipe.transform(studyID);
}

return this.permissionService.getRoles(tokenContext, organization, project, study);
}
}