From de4aa2e7842f1064fe3a8e6da004fb1dbe7c5020 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 15:38:34 -0500 Subject: [PATCH 01/10] Begin adding the ability to query for study permissions --- packages/server/schema.gql | 1 + .../src/permission/permission.module.ts | 3 ++- .../src/permission/permission.resolver.ts | 17 +++++++++++- .../src/permission/permission.service.ts | 27 ++++++++++++++++++- packages/server/src/study/study.module.ts | 4 +-- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 2894f351..c863e3bd 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -127,6 +127,7 @@ type Query { exists(name: String!): Boolean! getDatasets: [Dataset!]! getProjectPermissions(project: ID!): [Permission!]! + getStudyPermissions(study: ID!): [Permission!]! projectExists(name: String!): Boolean! getProjects: [Project!]! studyExists(name: String!, project: ID!): Boolean! diff --git a/packages/server/src/permission/permission.module.ts b/packages/server/src/permission/permission.module.ts index 950bee2b..0f78fdbd 100644 --- a/packages/server/src/permission/permission.module.ts +++ b/packages/server/src/permission/permission.module.ts @@ -4,9 +4,10 @@ import { PermissionResolver } from './permission.resolver'; import { PermissionService } from './permission.service'; import { ProjectModule } from '../project/project.module'; import { AuthModule } from '../auth/auth.module'; +import { StudyModule } from '../study/study.module'; @Module({ - imports: [forwardRef(() => ProjectModule), AuthModule], + imports: [forwardRef(() => ProjectModule), AuthModule, forwardRef(() => StudyModule)], providers: [casbinProvider, PermissionResolver, PermissionService], exports: [casbinProvider] }) diff --git a/packages/server/src/permission/permission.resolver.ts b/packages/server/src/permission/permission.resolver.ts index 734bad19..8e1b0d27 100644 --- a/packages/server/src/permission/permission.resolver.ts +++ b/packages/server/src/permission/permission.resolver.ts @@ -13,6 +13,8 @@ import * as casbin from 'casbin'; import { CASBIN_PROVIDER } from './casbin.provider'; import { Roles } from './permissions/roles'; import { ProjectPermissions } from './permissions/project'; +import { StudyPipe } from '../study/pipes/study.pipe'; +import { Study } from '../study/study.model'; @UseGuards(JwtAuthGuard) @Resolver(() => Permission) @@ -34,7 +36,7 @@ export class PermissionResolver { throw new UnauthorizedException('Requesting user is not an owner'); } - await this.permissionService.grantOwner(targetUser, requestingUser.id, organization._id); + await this.permissionService.grantOwner(targetUser, organization._id); return true; } @@ -67,6 +69,19 @@ export class PermissionResolver { return this.permissionService.grantProjectPermissions(project, user, isAdmin, requestingUser); } + @Query(() => [Permission]) + async getStudyPermissions( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @TokenContext() requestingUser: TokenPayload + ): Promise { + const hasPermission = await this.enforcer.enforce(requestingUser.id, ProjectPermissions.GRANT_ADMIN, study.project); + if (!hasPermission) { + throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); + } + + return this.permissionService.getStudyPermissions(study, requestingUser); + } + @ResolveField('user', () => UserModel) resolveUser(@Parent() permission: Permission): any { return { __typename: 'UserModel', id: permission.user }; diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index 408ce965..ca6c816f 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -6,6 +6,7 @@ import { UserService } from '../auth/services/user.service'; import { Project } from '../project/project.model'; import { TokenPayload } from '../jwt/token.dto'; import { Permission } from './permission.model'; +import { Study } from '../study/study.model'; @Injectable() export class PermissionService { @@ -15,7 +16,7 @@ export class PermissionService { ) {} /** requestingUser must be an owner themselves */ - async grantOwner(targetUser: string, requestingUser: string, organization: string): Promise { + async grantOwner(targetUser: string, organization: string): Promise { await this.enforcer.addPolicy(targetUser, Roles.OWNER, organization); } @@ -67,4 +68,28 @@ export class PermissionService { return true; } + + async getStudyPermissions(study: Study, requestingUser: TokenPayload): Promise { + // Get all the users associated with the organization + const users = await this.userService.getUsersForProject(requestingUser.projectId); + + // Create the cooresponding permission representation + const permissions = await Promise.all( + users.map(async (user) => { + const hasRole = await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study); + // Owner and project admins cannot be changed + const editable = !(await this.enforcer.enforce(user.id, Roles.OWNER, study)) && + !(await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, study)); + + return { + user: user.id, + role: Roles.STUDY_ADMIN, + hasRole, + editable + }; + }) + ); + + return permissions; + } } diff --git a/packages/server/src/study/study.module.ts b/packages/server/src/study/study.module.ts index 8ff10dd2..4d38bf84 100644 --- a/packages/server/src/study/study.module.ts +++ b/packages/server/src/study/study.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { StudyService } from './study.service'; import { StudyResolver } from './study.resolver'; import { MongooseModule } from '@nestjs/mongoose'; @@ -33,7 +33,7 @@ import { PermissionModule } from '../permission/permission.module'; ProjectModule, SharedModule, JwtModule, - PermissionModule + forwardRef(() => PermissionModule) ], providers: [StudyService, StudyResolver, StudyPipe, StudyCreatePipe], exports: [StudyService, StudyPipe] From e5d5c9ba7bcfcb97fcb3df9e81843610972d709a Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 16:03:56 -0500 Subject: [PATCH 02/10] Make dedicated resolvers for project, owner, and study permissions --- packages/server/schema.gql | 33 +++---- packages/server/src/auth/user.model.ts | 11 +++ .../src/permission/models/project.model.ts | 15 ++++ .../src/permission/models/study.model.ts | 0 .../server/src/permission/permission.model.ts | 31 ------- .../src/permission/permission.module.ts | 5 +- .../src/permission/permission.resolver.ts | 89 ------------------- .../src/permission/permission.service.ts | 18 ++-- .../permission/resolvers/owner.resolver.ts | 34 +++++++ .../permission/resolvers/project.resolver.ts | 54 +++++++++++ .../permission/resolvers/study.resolver.ts | 0 11 files changed, 138 insertions(+), 152 deletions(-) create mode 100644 packages/server/src/auth/user.model.ts create mode 100644 packages/server/src/permission/models/project.model.ts create mode 100644 packages/server/src/permission/models/study.model.ts delete mode 100644 packages/server/src/permission/permission.model.ts delete mode 100644 packages/server/src/permission/permission.resolver.ts create mode 100644 packages/server/src/permission/resolvers/owner.resolver.ts create mode 100644 packages/server/src/permission/resolvers/project.resolver.ts create mode 100644 packages/server/src/permission/resolvers/study.resolver.ts diff --git a/packages/server/schema.gql b/packages/server/schema.gql index c863e3bd..2139fae0 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -28,24 +28,6 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime -type UserModel { - id: ID! -} - -type Permission { - user: UserModel! - role: Roles! - hasRole: Boolean! - editable: Boolean! -} - -enum Roles { - OWNER - PROJECT_ADMIN - STUDY_ADMIN - CONTRIBUTOR -} - type TagSchema { dataSchema: JSON! uiSchema: JSON! @@ -66,6 +48,16 @@ type Study { tagsPerEntry: Float! } +type UserModel { + id: ID! +} + +type ProjectPermissionModel { + user: UserModel! + isProjectAdmin: Boolean! + editable: Boolean! +} + type Entry { _id: String! organization: ID! @@ -126,8 +118,7 @@ type Query { getOrganizations: [Organization!]! exists(name: String!): Boolean! getDatasets: [Dataset!]! - getProjectPermissions(project: ID!): [Permission!]! - getStudyPermissions(study: ID!): [Permission!]! + getProjectPermissions(project: ID!): [ProjectPermissionModel!]! projectExists(name: String!): Boolean! getProjects: [Project!]! studyExists(name: String!, project: ID!): Boolean! @@ -145,8 +136,8 @@ type Mutation { createDataset(dataset: DatasetCreate!): Dataset! changeDatasetName(dataset: ID!, newName: String!): Boolean! changeDatasetDescription(dataset: ID!, newDescription: String!): Boolean! - grantOwner(targetUser: ID!): Boolean! grantProjectPermissions(project: ID!, user: ID!, isAdmin: Boolean!): Boolean! + grantOwner(targetUser: ID!): Boolean! signLabCreateProject(project: ProjectCreate!): Project! deleteProject(project: ID!): Boolean! createStudy(study: StudyCreate!): Study! diff --git a/packages/server/src/auth/user.model.ts b/packages/server/src/auth/user.model.ts new file mode 100644 index 00000000..d93b1c3d --- /dev/null +++ b/packages/server/src/auth/user.model.ts @@ -0,0 +1,11 @@ +import { ObjectType, Field, Directive, ID } from '@nestjs/graphql'; + +/** Definition for external user */ +@ObjectType() +@Directive('@key(fields: "id")') +@Directive('@extends') +export class UserModel { + @Field(() => ID) + @Directive('@external') + id: string; +} diff --git a/packages/server/src/permission/models/project.model.ts b/packages/server/src/permission/models/project.model.ts new file mode 100644 index 00000000..8cd31966 --- /dev/null +++ b/packages/server/src/permission/models/project.model.ts @@ -0,0 +1,15 @@ +import { ObjectType, Field } from '@nestjs/graphql'; +import { UserModel } from '../../auth/user.model'; + + +@ObjectType() +export class ProjectPermissionModel { + @Field(() => UserModel) + user: string; + + @Field() + isProjectAdmin: boolean; + + @Field() + editable: boolean; +} diff --git a/packages/server/src/permission/models/study.model.ts b/packages/server/src/permission/models/study.model.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/permission/permission.model.ts b/packages/server/src/permission/permission.model.ts deleted file mode 100644 index 1b3eff1c..00000000 --- a/packages/server/src/permission/permission.model.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ObjectType, registerEnumType, Field, Directive, ID } from '@nestjs/graphql'; -import { Roles } from './permissions/roles'; - -registerEnumType(Roles, { - name: 'Roles' -}); - -/** Definition for external user */ -@ObjectType() -@Directive('@key(fields: "id")') -@Directive('@extends') -export class UserModel { - @Field(() => ID) - @Directive('@external') - id: string; -} - -@ObjectType() -export class Permission { - @Field(() => UserModel) - user: string; - - @Field(() => Roles) - role: Roles; - - @Field() - hasRole: boolean; - - @Field() - editable: boolean; -} diff --git a/packages/server/src/permission/permission.module.ts b/packages/server/src/permission/permission.module.ts index 0f78fdbd..4b26c82e 100644 --- a/packages/server/src/permission/permission.module.ts +++ b/packages/server/src/permission/permission.module.ts @@ -1,14 +1,15 @@ import { Module, forwardRef } from '@nestjs/common'; import { casbinProvider } from './casbin.provider'; -import { PermissionResolver } from './permission.resolver'; import { PermissionService } from './permission.service'; import { ProjectModule } from '../project/project.module'; import { AuthModule } from '../auth/auth.module'; import { StudyModule } from '../study/study.module'; +import { ProjectPermissionResolver } from './resolvers/project.resolver'; +import { OwnerPermissionResolver } from './resolvers/owner.resolver'; @Module({ imports: [forwardRef(() => ProjectModule), AuthModule, forwardRef(() => StudyModule)], - providers: [casbinProvider, PermissionResolver, PermissionService], + providers: [casbinProvider, PermissionService, ProjectPermissionResolver, OwnerPermissionResolver], exports: [casbinProvider] }) export class PermissionModule {} diff --git a/packages/server/src/permission/permission.resolver.ts b/packages/server/src/permission/permission.resolver.ts deleted file mode 100644 index 8e1b0d27..00000000 --- a/packages/server/src/permission/permission.resolver.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Resolver, Mutation, Args, ID, Query, ResolveField, Parent } from '@nestjs/graphql'; -import { JwtAuthGuard } from '../jwt/jwt.guard'; -import { UseGuards, Inject, UnauthorizedException } from '@nestjs/common'; -import { TokenContext } from '../jwt/token.context'; -import { TokenPayload } from '../jwt/token.dto'; -import { PermissionService } from './permission.service'; -import { OrganizationContext } from '../organization/organization.context'; -import { Organization } from '../organization/organization.model'; -import { ProjectPipe } from '../project/pipes/project.pipe'; -import { Project } from '../project/project.model'; -import { Permission, UserModel } from './permission.model'; -import * as casbin from 'casbin'; -import { CASBIN_PROVIDER } from './casbin.provider'; -import { Roles } from './permissions/roles'; -import { ProjectPermissions } from './permissions/project'; -import { StudyPipe } from '../study/pipes/study.pipe'; -import { Study } from '../study/study.model'; - -@UseGuards(JwtAuthGuard) -@Resolver(() => Permission) -export class PermissionResolver { - constructor( - private readonly permissionService: PermissionService, - @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer - ) {} - - @Mutation(() => Boolean) - async grantOwner( - @Args('targetUser', { type: () => ID }) targetUser: string, - @TokenContext() requestingUser: TokenPayload, - @OrganizationContext() organization: Organization - ): Promise { - // Make sure the requesting user is an owner - const isOwner = await this.enforcer.enforce(requestingUser, Roles.OWNER, organization); - if (!isOwner) { - throw new UnauthorizedException('Requesting user is not an owner'); - } - - await this.permissionService.grantOwner(targetUser, organization._id); - return true; - } - - @Query(() => [Permission]) - async getProjectPermissions( - @Args('project', { type: () => ID }, ProjectPipe) project: Project, - @TokenContext() requestingUser: TokenPayload - ): Promise { - // Make sure the user has the ability to manage project permissions - const hasPermission = await this.enforcer.enforce(requestingUser.id, ProjectPermissions.GRANT_ADMIN, project._id); - if (!hasPermission) { - throw new UnauthorizedException('Requesting user does not have permission to manage project permissions'); - } - - return this.permissionService.getProjectPermissions(project, requestingUser); - } - - @Mutation(() => Boolean) - async grantProjectPermissions( - @Args('project', { type: () => ID }, ProjectPipe) project: Project, - @Args('user', { type: () => ID }) user: string, - @Args('isAdmin', { type: () => Boolean }) isAdmin: boolean, - @TokenContext() requestingUser: TokenPayload - ): Promise { - const hasPermission = await this.enforcer.enforce(requestingUser.id, ProjectPermissions.GRANT_ADMIN, project._id); - if (!hasPermission) { - throw new UnauthorizedException('Requesting user does not have permission to manage project permissions'); - } - - return this.permissionService.grantProjectPermissions(project, user, isAdmin, requestingUser); - } - - @Query(() => [Permission]) - async getStudyPermissions( - @Args('study', { type: () => ID }, StudyPipe) study: Study, - @TokenContext() requestingUser: TokenPayload - ): Promise { - const hasPermission = await this.enforcer.enforce(requestingUser.id, ProjectPermissions.GRANT_ADMIN, study.project); - if (!hasPermission) { - throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); - } - - return this.permissionService.getStudyPermissions(study, requestingUser); - } - - @ResolveField('user', () => UserModel) - resolveUser(@Parent() permission: Permission): any { - return { __typename: 'UserModel', id: permission.user }; - } -} diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index ca6c816f..2452d6a4 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -5,8 +5,8 @@ import { Roles } from './permissions/roles'; import { UserService } from '../auth/services/user.service'; import { Project } from '../project/project.model'; import { TokenPayload } from '../jwt/token.dto'; -import { Permission } from './permission.model'; import { Study } from '../study/study.model'; +import { ProjectPermissionModel } from './models/project.model'; @Injectable() export class PermissionService { @@ -20,7 +20,7 @@ export class PermissionService { await this.enforcer.addPolicy(targetUser, Roles.OWNER, organization); } - async getProjectPermissions(project: Project, requestingUser: TokenPayload): Promise { + async getProjectPermissions(project: Project, requestingUser: TokenPayload): Promise { // Get all the users associated with the organization const users = await this.userService.getUsersForProject(requestingUser.projectId); @@ -32,8 +32,7 @@ export class PermissionService { return { user: user.id, - role: Roles.PROJECT_ADMIN, - hasRole, + isProjectAdmin: hasRole, editable }; }) @@ -69,17 +68,17 @@ export class PermissionService { return true; } - async getStudyPermissions(study: Study, requestingUser: TokenPayload): Promise { + async getStudyPermissions(study: Study, requestingUser: TokenPayload): Promise { // Get all the users associated with the organization const users = await this.userService.getUsersForProject(requestingUser.projectId); // Create the cooresponding permission representation const permissions = await Promise.all( users.map(async (user) => { - const hasRole = await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study); + const hasRole = await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString()); // Owner and project admins cannot be changed - const editable = !(await this.enforcer.enforce(user.id, Roles.OWNER, study)) && - !(await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, study)); + const editable = !(await this.enforcer.enforce(user.id, Roles.OWNER, study._id.toString())) && + !(await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, study._id.toString())); return { user: user.id, @@ -90,6 +89,7 @@ export class PermissionService { }) ); - return permissions; + // return permissions; + return true; } } diff --git a/packages/server/src/permission/resolvers/owner.resolver.ts b/packages/server/src/permission/resolvers/owner.resolver.ts new file mode 100644 index 00000000..97481416 --- /dev/null +++ b/packages/server/src/permission/resolvers/owner.resolver.ts @@ -0,0 +1,34 @@ +import { Args, Resolver, Mutation, ID } from '@nestjs/graphql'; +import { TokenContext } from '../../jwt/token.context'; +import { OrganizationContext } from '../../organization/organization.context'; +import { TokenPayload } from '../../jwt/token.dto'; +import { Organization } from '../../organization/organization.model'; +import * as casbin from 'casbin'; +import { CASBIN_PROVIDER } from '../casbin.provider'; +import { Inject, UnauthorizedException } from '@nestjs/common'; +import { Roles } from '../permissions/roles'; +import { PermissionService } from '../permission.service'; + +@Resolver() +export class OwnerPermissionResolver { + constructor( + @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, + private readonly permissionService: PermissionService + ) {} + + @Mutation(() => Boolean) + async grantOwner( + @Args('targetUser', { type: () => ID }) targetUser: string, + @TokenContext() requestingUser: TokenPayload, + @OrganizationContext() organization: Organization + ): Promise { + // Make sure the requesting user is an owner + const isOwner = await this.enforcer.enforce(requestingUser, Roles.OWNER, organization); + if (!isOwner) { + throw new UnauthorizedException('Requesting user is not an owner'); + } + + await this.permissionService.grantOwner(targetUser, organization._id); + return true; + } +} diff --git a/packages/server/src/permission/resolvers/project.resolver.ts b/packages/server/src/permission/resolvers/project.resolver.ts new file mode 100644 index 00000000..20a3454f --- /dev/null +++ b/packages/server/src/permission/resolvers/project.resolver.ts @@ -0,0 +1,54 @@ +import { Resolver, Mutation, Args, ID, Query, ResolveField, Parent } from '@nestjs/graphql'; +import { JwtAuthGuard } from '../../jwt/jwt.guard'; +import { UseGuards, Inject, UnauthorizedException } from '@nestjs/common'; +import { ProjectPermissionModel } from '../models/project.model'; +import { TokenContext } from '../../jwt/token.context'; +import { TokenPayload } from '../../jwt/token.dto'; +import { ProjectPipe } from '../../project/pipes/project.pipe'; +import { Project } from '../../project/project.model'; +import * as casbin from 'casbin'; +import { CASBIN_PROVIDER } from '../casbin.provider'; +import { ProjectPermissions } from '../permissions/project'; +import { PermissionService } from '../permission.service'; +import { UserModel } from '../../auth/user.model'; + +@UseGuards(JwtAuthGuard) +@Resolver(() => ProjectPermissionModel) +export class ProjectPermissionResolver { + constructor(@Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, + private readonly permissionService: PermissionService) {} + + @Query(() => [ProjectPermissionModel]) + async getProjectPermissions( + @Args('project', { type: () => ID }, ProjectPipe) project: Project, + @TokenContext() requestingUser: TokenPayload + ): Promise { + // Make sure the user has the ability to manage project permissions + const hasPermission = await this.enforcer.enforce(requestingUser.id, ProjectPermissions.GRANT_ADMIN, project._id); + if (!hasPermission) { + throw new UnauthorizedException('Requesting user does not have permission to manage project permissions'); + } + + return this.permissionService.getProjectPermissions(project, requestingUser); + } + + @Mutation(() => Boolean) + async grantProjectPermissions( + @Args('project', { type: () => ID }, ProjectPipe) project: Project, + @Args('user', { type: () => ID }) user: string, + @Args('isAdmin', { type: () => Boolean }) isAdmin: boolean, + @TokenContext() requestingUser: TokenPayload + ): Promise { + const hasPermission = await this.enforcer.enforce(requestingUser.id, ProjectPermissions.GRANT_ADMIN, project._id); + if (!hasPermission) { + throw new UnauthorizedException('Requesting user does not have permission to manage project permissions'); + } + + return this.permissionService.grantProjectPermissions(project, user, isAdmin, requestingUser); + } + + @ResolveField('user', () => UserModel) + resolveUser(@Parent() permission: ProjectPermissionModel): any { + return { __typename: 'UserModel', id: permission.user }; + } +} diff --git a/packages/server/src/permission/resolvers/study.resolver.ts b/packages/server/src/permission/resolvers/study.resolver.ts new file mode 100644 index 00000000..e69de29b From aaa5555fd921b563fbcb18a90355ac9422b0ec76 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 16:06:26 -0500 Subject: [PATCH 03/10] Update client for tweaks to permissions on project --- packages/client/src/graphql/graphql.ts | 24 +++++++------------ .../src/graphql/permission/permission.graphql | 5 ++-- .../src/graphql/permission/permission.ts | 5 ++-- .../pages/projects/ProjectUserPermissions.tsx | 8 +++---- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 1b13dfd2..3c844c66 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -417,14 +417,6 @@ export type OrganizationCreate = { projectId: Scalars['String']['input']; }; -export type Permission = { - __typename?: 'Permission'; - editable: Scalars['Boolean']['output']; - hasRole: Scalars['Boolean']['output']; - role: Roles; - user: UserModel; -}; - export type Project = { __typename?: 'Project'; _id: Scalars['ID']['output']; @@ -479,6 +471,13 @@ export type ProjectModel = { users: Array; }; +export type ProjectPermissionModel = { + __typename?: 'ProjectPermissionModel'; + editable: Scalars['Boolean']['output']; + isProjectAdmin: Scalars['Boolean']['output']; + user: UserModel; +}; + export type ProjectSettingsInput = { allowSignup?: InputMaybe; displayProjectName?: InputMaybe; @@ -501,7 +500,7 @@ export type Query = { getEntryUploadURL: Scalars['String']['output']; getOrganizations: Array; getProject: ProjectModel; - getProjectPermissions: Array; + getProjectPermissions: Array; getProjects: Array; getUser: UserModel; invite: InviteModel; @@ -611,13 +610,6 @@ export type ResetDto = { projectId: Scalars['String']['input']; }; -export enum Roles { - Contributor = 'CONTRIBUTOR', - Owner = 'OWNER', - ProjectAdmin = 'PROJECT_ADMIN', - StudyAdmin = 'STUDY_ADMIN' -} - export type Study = { __typename?: 'Study'; _id: Scalars['ID']['output']; diff --git a/packages/client/src/graphql/permission/permission.graphql b/packages/client/src/graphql/permission/permission.graphql index 7b838616..4fb56a6c 100644 --- a/packages/client/src/graphql/permission/permission.graphql +++ b/packages/client/src/graphql/permission/permission.graphql @@ -11,9 +11,8 @@ query getProjectPermissions($project: ID!) { updatedAt, deletedAt }, - hasRole, - editable, - role + isProjectAdmin, + editable } } diff --git a/packages/client/src/graphql/permission/permission.ts b/packages/client/src/graphql/permission/permission.ts index 17ebf1d9..7898d0ee 100644 --- a/packages/client/src/graphql/permission/permission.ts +++ b/packages/client/src/graphql/permission/permission.ts @@ -10,7 +10,7 @@ export type GetProjectPermissionsQueryVariables = Types.Exact<{ }>; -export type GetProjectPermissionsQuery = { __typename?: 'Query', getProjectPermissions: Array<{ __typename?: 'Permission', hasRole: boolean, editable: boolean, role: Types.Roles, user: { __typename?: 'UserModel', id: string, projectId: string, fullname?: string | null, username?: string | null, email?: string | null, role: number, createdAt: any, updatedAt: any, deletedAt?: any | null } }> }; +export type GetProjectPermissionsQuery = { __typename?: 'Query', getProjectPermissions: Array<{ __typename?: 'ProjectPermissionModel', isProjectAdmin: boolean, editable: boolean, user: { __typename?: 'UserModel', id: string, projectId: string, fullname?: string | null, username?: string | null, email?: string | null, role: number, createdAt: any, updatedAt: any, deletedAt?: any | null } }> }; export type GrantProjectPermissionsMutationVariables = Types.Exact<{ project: Types.Scalars['ID']['input']; @@ -36,9 +36,8 @@ export const GetProjectPermissionsDocument = gql` updatedAt deletedAt } - hasRole + isProjectAdmin editable - role } } `; diff --git a/packages/client/src/pages/projects/ProjectUserPermissions.tsx b/packages/client/src/pages/projects/ProjectUserPermissions.tsx index 05b4f2ba..4d3a9674 100644 --- a/packages/client/src/pages/projects/ProjectUserPermissions.tsx +++ b/packages/client/src/pages/projects/ProjectUserPermissions.tsx @@ -2,7 +2,7 @@ import { Switch, Typography } from '@mui/material'; import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { ChangeEvent, useEffect, useState } from 'react'; import { useProject } from '../../context/Project.context'; -import { Permission, Project } from '../../graphql/graphql'; +import { ProjectPermissionModel, Project } from '../../graphql/graphql'; import { useGetProjectPermissionsQuery } from '../../graphql/permission/permission'; import { DecodedToken, useAuth } from '../../context/Auth.context'; import { useGrantProjectPermissionsMutation } from '../../graphql/permission/permission'; @@ -19,7 +19,7 @@ export const ProjectUserPermissions: React.FC = () => { }; interface EditAdminSwitchProps { - permission: Permission; + permission: ProjectPermissionModel; currentUser: DecodedToken; project: Project; refetch: () => void; @@ -46,7 +46,7 @@ const EditAdminSwitch: React.FC = (props) => { return ( @@ -60,7 +60,7 @@ const UserPermissionTable: React.FC<{ project: Project }> = ({ project }) => { } }); - const [rows, setRows] = useState([]); + const [rows, setRows] = useState([]); const { decodedToken } = useAuth(); useEffect(() => { From 3d72c3cc217ce82108eae998d9ab7075ab4deee5 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 16:11:07 -0500 Subject: [PATCH 04/10] Add new role --- .../src/permission/models/study.model.ts | 27 +++++++++++++++++++ .../src/permission/permissions/project.ts | 2 ++ .../src/permission/permissions/roles.ts | 1 + .../src/permission/permissions/study.ts | 2 ++ .../server/src/permission/permissions/tag.ts | 4 ++- 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/server/src/permission/models/study.model.ts b/packages/server/src/permission/models/study.model.ts index e69de29b..ebe889d8 100644 --- a/packages/server/src/permission/models/study.model.ts +++ b/packages/server/src/permission/models/study.model.ts @@ -0,0 +1,27 @@ +import { ObjectType, Field } from '@nestjs/graphql'; +import { UserModel } from '../../auth/user.model'; + + +@ObjectType() +export class StudyPermissionModel { + @Field(() => UserModel) + user: string; + + @Field() + isStudyAdmin: boolean; + + @Field() + isStudyAdminEditable: boolean; + + @Field() + isContributor: boolean; + + @Field() + isContributorEditable: boolean; + + @Field() + isTrained: boolean; + + @Field() + isTrainedEditable: boolean; +} diff --git a/packages/server/src/permission/permissions/project.ts b/packages/server/src/permission/permissions/project.ts index 3fa625b1..b4f56cd0 100644 --- a/packages/server/src/permission/permissions/project.ts +++ b/packages/server/src/permission/permissions/project.ts @@ -23,4 +23,6 @@ export const roleToProjectPermissions: string[][] = [ // CONTRIBUTOR permissions [Roles.CONTRIBUTOR, ProjectPermissions.READ] + + // TRAINED_CONTRIBUTOR permissions ]; diff --git a/packages/server/src/permission/permissions/roles.ts b/packages/server/src/permission/permissions/roles.ts index f228a43a..10586534 100644 --- a/packages/server/src/permission/permissions/roles.ts +++ b/packages/server/src/permission/permissions/roles.ts @@ -2,6 +2,7 @@ export enum Roles { OWNER = 'owner', PROJECT_ADMIN = 'project_admin', STUDY_ADMIN = 'study_admin', + TRAINED_CONTRIBUTOR = 'trained_contributor', CONTRIBUTOR = 'contributor' } diff --git a/packages/server/src/permission/permissions/study.ts b/packages/server/src/permission/permissions/study.ts index df831450..e9e78679 100644 --- a/packages/server/src/permission/permissions/study.ts +++ b/packages/server/src/permission/permissions/study.ts @@ -23,4 +23,6 @@ export const roleToStudyPermissions: string[][] = [ // CONTRIBUTOR permissions [Roles.CONTRIBUTOR, StudyPermissions.READ] + + // TRAINED_CONTRIBUTOR permissions ]; diff --git a/packages/server/src/permission/permissions/tag.ts b/packages/server/src/permission/permissions/tag.ts index 81715590..71177fa9 100644 --- a/packages/server/src/permission/permissions/tag.ts +++ b/packages/server/src/permission/permissions/tag.ts @@ -18,5 +18,7 @@ export const roleToTagPermissions: string[][] = [ [Roles.STUDY_ADMIN, TagPermissions.UPDATE], // CONTRIBUTOR permissions - [Roles.CONTRIBUTOR, TagPermissions.CREATE] + + // TRAINED_CONTRIBUTOR permissions + [Roles.TRAINED_CONTRIBUTOR, TagPermissions.CREATE] ]; From e22535d34defb183fbd9b72a2cf33bee6342675d Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 16:18:27 -0500 Subject: [PATCH 05/10] Add study resolver --- packages/server/schema.gql | 11 ++++++ .../src/permission/permission.module.ts | 3 +- .../src/permission/permission.service.ts | 25 +++++++----- .../permission/resolvers/study.resolver.ts | 39 +++++++++++++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 2139fae0..2657a2da 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -58,6 +58,16 @@ type ProjectPermissionModel { editable: Boolean! } +type StudyPermissionModel { + user: UserModel! + isStudyAdmin: Boolean! + isStudyAdminEditable: Boolean! + isContributor: Boolean! + isContributorEditable: Boolean! + isTrained: Boolean! + isTrainedEditable: Boolean! +} + type Entry { _id: String! organization: ID! @@ -119,6 +129,7 @@ type Query { exists(name: String!): Boolean! getDatasets: [Dataset!]! getProjectPermissions(project: ID!): [ProjectPermissionModel!]! + getStudyPermissions(study: ID!): [StudyPermissionModel!]! projectExists(name: String!): Boolean! getProjects: [Project!]! studyExists(name: String!, project: ID!): Boolean! diff --git a/packages/server/src/permission/permission.module.ts b/packages/server/src/permission/permission.module.ts index 4b26c82e..a3eadad0 100644 --- a/packages/server/src/permission/permission.module.ts +++ b/packages/server/src/permission/permission.module.ts @@ -6,10 +6,11 @@ import { AuthModule } from '../auth/auth.module'; import { StudyModule } from '../study/study.module'; import { ProjectPermissionResolver } from './resolvers/project.resolver'; import { OwnerPermissionResolver } from './resolvers/owner.resolver'; +import { StudyPermissionResolver } from './resolvers/study.resolver'; @Module({ imports: [forwardRef(() => ProjectModule), AuthModule, forwardRef(() => StudyModule)], - providers: [casbinProvider, PermissionService, ProjectPermissionResolver, OwnerPermissionResolver], + providers: [casbinProvider, PermissionService, ProjectPermissionResolver, OwnerPermissionResolver, StudyPermissionResolver], exports: [casbinProvider] }) export class PermissionModule {} diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index 2452d6a4..05458575 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -7,6 +7,7 @@ import { Project } from '../project/project.model'; import { TokenPayload } from '../jwt/token.dto'; import { Study } from '../study/study.model'; import { ProjectPermissionModel } from './models/project.model'; +import { StudyPermissionModel } from './models/study.model'; @Injectable() export class PermissionService { @@ -68,28 +69,34 @@ export class PermissionService { return true; } - async getStudyPermissions(study: Study, requestingUser: TokenPayload): Promise { + async getStudyPermissions(study: Study, requestingUser: TokenPayload): Promise { // Get all the users associated with the organization const users = await this.userService.getUsersForProject(requestingUser.projectId); // Create the cooresponding permission representation const permissions = await Promise.all( users.map(async (user) => { - const hasRole = await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString()); - // Owner and project admins cannot be changed - const editable = !(await this.enforcer.enforce(user.id, Roles.OWNER, study._id.toString())) && - !(await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, study._id.toString())); + const isStudyAdmin = await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString()); + const isStudyAdminEditable = !(await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, study._id.toString())); + + const isContributor = await this.enforcer.enforce(user.id, Roles.CONTRIBUTOR, study._id.toString()); + const isContributorEditable = !(await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString())); + + const isTrained = await this.enforcer.enforce(user.id, Roles.TRAINED_CONTRIBUTOR, study._id.toString()); return { user: user.id, - role: Roles.STUDY_ADMIN, - hasRole, - editable + isStudyAdmin, + isStudyAdminEditable, + isContributor, + isContributorEditable, + isTrained, + isTrainedEditable: true }; }) ); // return permissions; - return true; + return permissions; } } diff --git a/packages/server/src/permission/resolvers/study.resolver.ts b/packages/server/src/permission/resolvers/study.resolver.ts index e69de29b..26ca38a6 100644 --- a/packages/server/src/permission/resolvers/study.resolver.ts +++ b/packages/server/src/permission/resolvers/study.resolver.ts @@ -0,0 +1,39 @@ +import { Resolver, Args, ID, Query, ResolveField, Parent } from '@nestjs/graphql'; +import { JwtAuthGuard } from '../../jwt/jwt.guard'; +import { UseGuards, Inject, UnauthorizedException } from '@nestjs/common'; +import { StudyPermissionModel } from '../models/study.model'; +import { TokenContext } from '../../jwt/token.context'; +import { TokenPayload } from '../../jwt/token.dto'; +import { StudyPipe } from '../../study/pipes/study.pipe'; +import { Study } from '../../study/study.model'; +import * as casbin from 'casbin'; +import { CASBIN_PROVIDER } from '../casbin.provider'; +import { StudyPermissions } from '../permissions/study'; +import { PermissionService } from '../permission.service'; +import { UserModel } from '../../auth/user.model'; + +@UseGuards(JwtAuthGuard) +@Resolver(() => StudyPermissionModel) +export class StudyPermissionResolver { + constructor(@Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, + private readonly permissionService: PermissionService) {} + + @Query(() => [StudyPermissionModel]) + async getStudyPermissions( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @TokenContext() requestingUser: TokenPayload + ): Promise { + // Make sure the user has the ability to manage study permissions + const hasPermission = await this.enforcer.enforce(requestingUser.id, StudyPermissions.GRANT_ACCESS, study._id); + if (!hasPermission) { + throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); + } + + return this.permissionService.getStudyPermissions(study, requestingUser); + } + + @ResolveField('user', () => UserModel) + resolveUser(@Parent() permission: StudyPermissionModel): any { + return { __typename: 'UserModel', id: permission.user }; + } +} From 1d08e5d60461570decf4171de0ffd7b05f81d48c Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 16:32:16 -0500 Subject: [PATCH 06/10] Visualizing study admin roles in client --- packages/client/src/graphql/graphql.ts | 17 ++ .../src/graphql/permission/permission.graphql | 22 ++ .../src/graphql/permission/permission.ts | 60 +++++- .../src/pages/studies/UserPermissions.tsx | 198 +++++++----------- 4 files changed, 168 insertions(+), 129 deletions(-) diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 3c844c66..88a83156 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -502,6 +502,7 @@ export type Query = { getProject: ProjectModel; getProjectPermissions: Array; getProjects: Array; + getStudyPermissions: Array; getUser: UserModel; invite: InviteModel; invites: Array; @@ -556,6 +557,11 @@ export type QueryGetProjectPermissionsArgs = { }; +export type QueryGetStudyPermissionsArgs = { + study: Scalars['ID']['input']; +}; + + export type QueryGetUserArgs = { id: Scalars['ID']['input']; }; @@ -630,6 +636,17 @@ export type StudyCreate = { tagsPerEntry: Scalars['Float']['input']; }; +export type StudyPermissionModel = { + __typename?: 'StudyPermissionModel'; + isContributor: Scalars['Boolean']['output']; + isContributorEditable: Scalars['Boolean']['output']; + isStudyAdmin: Scalars['Boolean']['output']; + isStudyAdminEditable: Scalars['Boolean']['output']; + isTrained: Scalars['Boolean']['output']; + isTrainedEditable: Scalars['Boolean']['output']; + user: UserModel; +}; + export type Tag = { __typename?: 'Tag'; _id: Scalars['String']['output']; diff --git a/packages/client/src/graphql/permission/permission.graphql b/packages/client/src/graphql/permission/permission.graphql index 4fb56a6c..10f919d6 100644 --- a/packages/client/src/graphql/permission/permission.graphql +++ b/packages/client/src/graphql/permission/permission.graphql @@ -19,3 +19,25 @@ query getProjectPermissions($project: ID!) { mutation grantProjectPermissions($project: ID!, $user: ID!, $isAdmin: Boolean!) { grantProjectPermissions(project: $project, user: $user, isAdmin: $isAdmin) } + +query getStudyPermissions($study: ID!) { + getStudyPermissions(study: $study) { + user { + id, + projectId, + fullname, + username, + email, + role, + createdAt, + updatedAt, + deletedAt + }, + isStudyAdmin, + isStudyAdminEditable, + isContributor, + isContributorEditable, + isTrained, + isTrainedEditable + } +} diff --git a/packages/client/src/graphql/permission/permission.ts b/packages/client/src/graphql/permission/permission.ts index 7898d0ee..56267b13 100644 --- a/packages/client/src/graphql/permission/permission.ts +++ b/packages/client/src/graphql/permission/permission.ts @@ -21,6 +21,13 @@ export type GrantProjectPermissionsMutationVariables = Types.Exact<{ export type GrantProjectPermissionsMutation = { __typename?: 'Mutation', grantProjectPermissions: boolean }; +export type GetStudyPermissionsQueryVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; +}>; + + +export type GetStudyPermissionsQuery = { __typename?: 'Query', getStudyPermissions: Array<{ __typename?: 'StudyPermissionModel', isStudyAdmin: boolean, isStudyAdminEditable: boolean, isContributor: boolean, isContributorEditable: boolean, isTrained: boolean, isTrainedEditable: boolean, user: { __typename?: 'UserModel', id: string, projectId: string, fullname?: string | null, username?: string | null, email?: string | null, role: number, createdAt: any, updatedAt: any, deletedAt?: any | null } }> }; + export const GetProjectPermissionsDocument = gql` query getProjectPermissions($project: ID!) { @@ -101,4 +108,55 @@ export function useGrantProjectPermissionsMutation(baseOptions?: Apollo.Mutation } export type GrantProjectPermissionsMutationHookResult = ReturnType; export type GrantProjectPermissionsMutationResult = Apollo.MutationResult; -export type GrantProjectPermissionsMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file +export type GrantProjectPermissionsMutationOptions = Apollo.BaseMutationOptions; +export const GetStudyPermissionsDocument = gql` + query getStudyPermissions($study: ID!) { + getStudyPermissions(study: $study) { + user { + id + projectId + fullname + username + email + role + createdAt + updatedAt + deletedAt + } + isStudyAdmin + isStudyAdminEditable + isContributor + isContributorEditable + isTrained + isTrainedEditable + } +} + `; + +/** + * __useGetStudyPermissionsQuery__ + * + * To run a query within a React component, call `useGetStudyPermissionsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetStudyPermissionsQuery` 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 } = useGetStudyPermissionsQuery({ + * variables: { + * study: // value for 'study' + * }, + * }); + */ +export function useGetStudyPermissionsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetStudyPermissionsDocument, options); + } +export function useGetStudyPermissionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetStudyPermissionsDocument, options); + } +export type GetStudyPermissionsQueryHookResult = ReturnType; +export type GetStudyPermissionsLazyQueryHookResult = ReturnType; +export type GetStudyPermissionsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/packages/client/src/pages/studies/UserPermissions.tsx b/packages/client/src/pages/studies/UserPermissions.tsx index 2d7d22a0..90ad7531 100644 --- a/packages/client/src/pages/studies/UserPermissions.tsx +++ b/packages/client/src/pages/studies/UserPermissions.tsx @@ -1,152 +1,94 @@ import { Switch, Typography } from '@mui/material'; -import useEnhancedEffect from '@mui/material/utils/useEnhancedEffect'; -import { DataGrid, GridColDef, GridRenderCellParams, useGridApiContext } from '@mui/x-data-grid'; -import { GridRowModesModel } from '@mui/x-data-grid-pro'; -import { useRef, useState } from 'react'; +import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; +import { useStudy } from '../../context/Study.context'; +import { Study, StudyPermissionModel } from '../../graphql/graphql'; +import { DecodedToken, useAuth } from '../../context/Auth.context'; +import { useGetStudyPermissionsQuery } from '../../graphql/permission/permission'; +import { useEffect, useState } from 'react'; -const SwitchEditInputCell: React.FC = (props: GridRenderCellParams) => { - const { id, value, field, hasFocus } = props; - const apiRef = useGridApiContext(); - const ref = useRef(); - const handleChange = (newValue: boolean | false) => { - apiRef.current.setEditCellValue({ id, field, value: newValue }); - }; +export const StudyUserPermissions: React.FC = () => { + const { study } = useStudy(); - useEnhancedEffect(() => { - if (hasFocus && ref.current) { - const input = ref.current.querySelector(`input[value="${value}"]`); - input?.focus(); - } - }, [hasFocus, value]); + return ( + <> + User Permissions + {study && } + + ); +}; - return handleChange} />; +interface EditStudyAdminSwitchProps { + study: Study; + permission: StudyPermissionModel; + currentUser: DecodedToken; +} + +const EditStudyAdminSwitch: React.FC = (props) => { + + return ( + + ); }; -const tableRows = [ - { - id: 1, - name: 'Prof Appavoo', - username: 'appavoo', - email: 'appavoo@bread.com', - adminSwitch: true, - visibleSwitch: false, - switch: true - }, - { - id: 2, - name: 'Heather', - username: 'Heather82', - email: 'heather@hotmail.com', - adminSwitch: false, - visibleSwitch: true, - switch: true - }, - { - id: 3, - name: 'Kamila', - username: 'kamila0509', - email: 'kamila@gmail.com' - }, - { - id: 4, - name: 'Mr Ronaldinho', - username: 'ron12345', - email: 'ron@bu.edu', - adminSwitch: true, - visibleSwitch: false, - switch: true - } -]; -export const StudyUserPermissions: React.FC = () => { - const [rows] = useState(tableRows); - const [rowModesModel, setRowModesModel] = useState({}); +const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { + const { decodedToken } = useAuth(); + const { data, refetch } = useGetStudyPermissionsQuery({ + variables: { + study: study._id + } + }); - const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - setRowModesModel(newRowModesModel); - }; + const [permissions, setPermissions] = useState([]); + + useEffect(() => { + if (data) { + setPermissions(data.getStudyPermissions); + } + }, [data]); const columns: GridColDef[] = [ - { - field: 'id', - headerName: 'ID', - flex: 1, - maxWidth: 100 - }, - { - field: 'name', - headerName: 'Name', - editable: true, - flex: 1, - maxWidth: 300 - }, - { - field: 'username', - headerName: 'Username', - editable: true, - flex: 1, - maxWidth: 300 - }, { field: 'email', headerName: 'Email', - flex: 1, - maxWidth: 300, - editable: true + valueGetter: (params) => params.row.user.email, + flex: 1.75, + editable: false }, { - field: 'adminSwitch', + field: 'studyAdmin', type: 'boolean', - editable: true, - maxWidth: 200, - flex: 1, headerName: 'Study Admin', - renderCell: (params) => , - renderEditCell: (params) => - }, - { - field: 'visibleSwitch', - type: 'boolean', - editable: true, - headerName: 'Study Visible', - renderCell: (params) => , - maxWidth: 200, - flex: 1, - renderEditCell: (params) => - }, - { - field: 'switch', - type: 'boolean', - editable: true, - maxWidth: 200, - flex: 1, - headerName: 'Contribute', - renderCell: (params) => , - renderEditCell: (params) => + valueGetter: (params) => params.row.isStudyAdmin, + renderCell: (params: GridRenderCellParams) => { + return ( + + ); + }, + editable: false, + flex: 1 } ]; - return ( - <> - User Permissions - 'auto'} - rows={rows} - columns={columns} - rowModesModel={rowModesModel} - onRowModesModelChange={handleRowModesModelChange} - initialState={{ - pagination: { - paginationModel: { - pageSize: 5 - } + 'auto'} + rows={permissions} + columns={columns} + getRowId={(row) => row.user.id} + initialState={{ + pagination: { + paginationModel: { + pageSize: 5 } - }} - pageSizeOptions={[5]} - checkboxSelection - disableRowSelectionOnClick - /> - + } + }} + pageSizeOptions={[5]} + checkboxSelection + disableRowSelectionOnClick + /> ); }; From afa159acea036aad7a092d15ab7eab7b9fe245ee Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 16:36:45 -0500 Subject: [PATCH 07/10] Woring visualization of all study roles --- .../src/pages/studies/UserPermissions.tsx | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/client/src/pages/studies/UserPermissions.tsx b/packages/client/src/pages/studies/UserPermissions.tsx index 90ad7531..f612be99 100644 --- a/packages/client/src/pages/studies/UserPermissions.tsx +++ b/packages/client/src/pages/studies/UserPermissions.tsx @@ -29,7 +29,27 @@ const EditStudyAdminSwitch: React.FC = (props) => { return ( + ); +}; + +const EditContributorSwitch: React.FC = (props) => { + + return ( + + ); +}; + +const EditTrainedSwitch: React.FC = (props) => { + + return ( + ); }; @@ -71,6 +91,30 @@ const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { }, editable: false, flex: 1 + }, + { + field: 'contributor', + headerName: 'Contributor', + valueGetter: (params) => params.row.isContributor, + renderCell: (params: GridRenderCellParams) => { + return ( + + ); + }, + editable: false, + flex: 1 + }, + { + field: 'trained', + headerName: 'Trained', + valueGetter: (params) => params.row.isTrained, + renderCell: (params: GridRenderCellParams) => { + return ( + + ); + }, + editable: false, + flex: 1 } ]; return ( From 281f0616b3126fc63ee14a60debdd5cc45a0f8ca Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 16:56:53 -0500 Subject: [PATCH 08/10] Edit of study admin, contributor, and trained contributor --- packages/client/src/graphql/graphql.ts | 24 ++++ .../src/graphql/permission/permission.graphql | 12 ++ .../src/graphql/permission/permission.ts | 128 +++++++++++++++++- .../src/pages/studies/UserPermissions.tsx | 83 ++++++++++-- packages/server/schema.gql | 3 + .../src/permission/permission.service.ts | 68 ++++++++++ .../permission/resolvers/study.resolver.ts | 50 ++++++- 7 files changed, 352 insertions(+), 16 deletions(-) diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 88a83156..60097ca4 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -189,8 +189,11 @@ export type Mutation = { deleteProject: Scalars['Boolean']['output']; deleteStudy: Scalars['Boolean']['output']; forgotPassword: Scalars['Boolean']['output']; + grantContributor: Scalars['Boolean']['output']; grantOwner: Scalars['Boolean']['output']; grantProjectPermissions: Scalars['Boolean']['output']; + grantStudyAdmin: Scalars['Boolean']['output']; + grantTrainedContributor: Scalars['Boolean']['output']; lexiconAddEntry: LexiconEntry; /** Remove all entries from a given lexicon */ lexiconClearEntries: Scalars['Boolean']['output']; @@ -312,6 +315,13 @@ export type MutationForgotPasswordArgs = { }; +export type MutationGrantContributorArgs = { + isContributor: Scalars['Boolean']['input']; + study: Scalars['ID']['input']; + user: Scalars['ID']['input']; +}; + + export type MutationGrantOwnerArgs = { targetUser: Scalars['ID']['input']; }; @@ -324,6 +334,20 @@ export type MutationGrantProjectPermissionsArgs = { }; +export type MutationGrantStudyAdminArgs = { + isAdmin: Scalars['Boolean']['input']; + study: Scalars['ID']['input']; + user: Scalars['ID']['input']; +}; + + +export type MutationGrantTrainedContributorArgs = { + isTrained: Scalars['Boolean']['input']; + study: Scalars['ID']['input']; + user: Scalars['ID']['input']; +}; + + export type MutationLexiconAddEntryArgs = { entry: LexiconAddEntry; }; diff --git a/packages/client/src/graphql/permission/permission.graphql b/packages/client/src/graphql/permission/permission.graphql index 10f919d6..cc83040e 100644 --- a/packages/client/src/graphql/permission/permission.graphql +++ b/packages/client/src/graphql/permission/permission.graphql @@ -41,3 +41,15 @@ query getStudyPermissions($study: ID!) { isTrainedEditable } } + +mutation grantStudyAdmin($study: ID!, $user: ID!, $isAdmin: Boolean!) { + grantStudyAdmin(study: $study, user: $user, isAdmin: $isAdmin) +} + +mutation grantContributor($study: ID!, $user: ID!, $isContributor: Boolean!) { + grantContributor(study: $study, user: $user, isContributor: $isContributor) +} + +mutation grantTrainedContributor($study: ID!, $user: ID!, $isTrained: Boolean!) { + grantTrainedContributor(study: $study, user: $user, isTrained: $isTrained) +} diff --git a/packages/client/src/graphql/permission/permission.ts b/packages/client/src/graphql/permission/permission.ts index 56267b13..1ff00999 100644 --- a/packages/client/src/graphql/permission/permission.ts +++ b/packages/client/src/graphql/permission/permission.ts @@ -28,6 +28,33 @@ export type GetStudyPermissionsQueryVariables = Types.Exact<{ export type GetStudyPermissionsQuery = { __typename?: 'Query', getStudyPermissions: Array<{ __typename?: 'StudyPermissionModel', isStudyAdmin: boolean, isStudyAdminEditable: boolean, isContributor: boolean, isContributorEditable: boolean, isTrained: boolean, isTrainedEditable: boolean, user: { __typename?: 'UserModel', id: string, projectId: string, fullname?: string | null, username?: string | null, email?: string | null, role: number, createdAt: any, updatedAt: any, deletedAt?: any | null } }> }; +export type GrantStudyAdminMutationVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; + user: Types.Scalars['ID']['input']; + isAdmin: Types.Scalars['Boolean']['input']; +}>; + + +export type GrantStudyAdminMutation = { __typename?: 'Mutation', grantStudyAdmin: boolean }; + +export type GrantContributorMutationVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; + user: Types.Scalars['ID']['input']; + isContributor: Types.Scalars['Boolean']['input']; +}>; + + +export type GrantContributorMutation = { __typename?: 'Mutation', grantContributor: boolean }; + +export type GrantTrainedContributorMutationVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; + user: Types.Scalars['ID']['input']; + isTrained: Types.Scalars['Boolean']['input']; +}>; + + +export type GrantTrainedContributorMutation = { __typename?: 'Mutation', grantTrainedContributor: boolean }; + export const GetProjectPermissionsDocument = gql` query getProjectPermissions($project: ID!) { @@ -159,4 +186,103 @@ export function useGetStudyPermissionsLazyQuery(baseOptions?: Apollo.LazyQueryHo } export type GetStudyPermissionsQueryHookResult = ReturnType; export type GetStudyPermissionsLazyQueryHookResult = ReturnType; -export type GetStudyPermissionsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetStudyPermissionsQueryResult = Apollo.QueryResult; +export const GrantStudyAdminDocument = gql` + mutation grantStudyAdmin($study: ID!, $user: ID!, $isAdmin: Boolean!) { + grantStudyAdmin(study: $study, user: $user, isAdmin: $isAdmin) +} + `; +export type GrantStudyAdminMutationFn = Apollo.MutationFunction; + +/** + * __useGrantStudyAdminMutation__ + * + * To run a mutation, you first call `useGrantStudyAdminMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGrantStudyAdminMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [grantStudyAdminMutation, { data, loading, error }] = useGrantStudyAdminMutation({ + * variables: { + * study: // value for 'study' + * user: // value for 'user' + * isAdmin: // value for 'isAdmin' + * }, + * }); + */ +export function useGrantStudyAdminMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GrantStudyAdminDocument, options); + } +export type GrantStudyAdminMutationHookResult = ReturnType; +export type GrantStudyAdminMutationResult = Apollo.MutationResult; +export type GrantStudyAdminMutationOptions = Apollo.BaseMutationOptions; +export const GrantContributorDocument = gql` + mutation grantContributor($study: ID!, $user: ID!, $isContributor: Boolean!) { + grantContributor(study: $study, user: $user, isContributor: $isContributor) +} + `; +export type GrantContributorMutationFn = Apollo.MutationFunction; + +/** + * __useGrantContributorMutation__ + * + * To run a mutation, you first call `useGrantContributorMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGrantContributorMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [grantContributorMutation, { data, loading, error }] = useGrantContributorMutation({ + * variables: { + * study: // value for 'study' + * user: // value for 'user' + * isContributor: // value for 'isContributor' + * }, + * }); + */ +export function useGrantContributorMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GrantContributorDocument, options); + } +export type GrantContributorMutationHookResult = ReturnType; +export type GrantContributorMutationResult = Apollo.MutationResult; +export type GrantContributorMutationOptions = Apollo.BaseMutationOptions; +export const GrantTrainedContributorDocument = gql` + mutation grantTrainedContributor($study: ID!, $user: ID!, $isTrained: Boolean!) { + grantTrainedContributor(study: $study, user: $user, isTrained: $isTrained) +} + `; +export type GrantTrainedContributorMutationFn = Apollo.MutationFunction; + +/** + * __useGrantTrainedContributorMutation__ + * + * To run a mutation, you first call `useGrantTrainedContributorMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGrantTrainedContributorMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [grantTrainedContributorMutation, { data, loading, error }] = useGrantTrainedContributorMutation({ + * variables: { + * study: // value for 'study' + * user: // value for 'user' + * isTrained: // value for 'isTrained' + * }, + * }); + */ +export function useGrantTrainedContributorMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GrantTrainedContributorDocument, options); + } +export type GrantTrainedContributorMutationHookResult = ReturnType; +export type GrantTrainedContributorMutationResult = Apollo.MutationResult; +export type GrantTrainedContributorMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file diff --git a/packages/client/src/pages/studies/UserPermissions.tsx b/packages/client/src/pages/studies/UserPermissions.tsx index f612be99..b7ffa4cc 100644 --- a/packages/client/src/pages/studies/UserPermissions.tsx +++ b/packages/client/src/pages/studies/UserPermissions.tsx @@ -3,7 +3,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { useStudy } from '../../context/Study.context'; import { Study, StudyPermissionModel } from '../../graphql/graphql'; import { DecodedToken, useAuth } from '../../context/Auth.context'; -import { useGetStudyPermissionsQuery } from '../../graphql/permission/permission'; +import { useGetStudyPermissionsQuery, useGrantStudyAdminMutation, useGrantContributorMutation, useGrantTrainedContributorMutation } from '../../graphql/permission/permission'; import { useEffect, useState } from 'react'; @@ -18,38 +18,93 @@ export const StudyUserPermissions: React.FC = () => { ); }; -interface EditStudyAdminSwitchProps { +interface EditSwitchProps { study: Study; permission: StudyPermissionModel; currentUser: DecodedToken; + refetch: () => void; } -const EditStudyAdminSwitch: React.FC = (props) => { +const EditStudyAdminSwitch: React.FC = (props) => { + const [grantStudyAdmin, grantStudyAdminResults] = useGrantStudyAdminMutation(); + + const handleChange = (event: React.ChangeEvent) => { + grantStudyAdmin({ + variables: { + study: props.study._id, + user: props.permission.user.id, + isAdmin: event.target.checked + } + }); + }; + + useEffect(() => { + if (grantStudyAdminResults.data) { + props.refetch(); + } + }, [grantStudyAdminResults]); return ( ); }; -const EditContributorSwitch: React.FC = (props) => { +const EditContributorSwitch: React.FC = (props) => { + const [grantContributor, grantContributorResults] = useGrantContributorMutation(); + + const handleChange = (event: React.ChangeEvent) => { + grantContributor({ + variables: { + study: props.study._id, + user: props.permission.user.id, + isContributor: event.target.checked + } + }); + }; + + useEffect(() => { + if (grantContributorResults.data) { + props.refetch(); + } + }, [grantContributorResults]); - return ( - - ); + return ( + + ); }; -const EditTrainedSwitch: React.FC = (props) => { +const EditTrainedSwitch: React.FC = (props) => { + const [grantTrainedContributor, grantTrainedContributorResults] = useGrantTrainedContributorMutation(); + + const handleChange = (event: React.ChangeEvent) => { + grantTrainedContributor({ + variables: { + study: props.study._id, + user: props.permission.user.id, + isTrained: event.target.checked + } + }); + }; + + useEffect(() => { + if (grantTrainedContributorResults.data) { + props.refetch(); + } + }, [grantTrainedContributorResults]); return ( ); }; @@ -86,7 +141,7 @@ const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { valueGetter: (params) => params.row.isStudyAdmin, renderCell: (params: GridRenderCellParams) => { return ( - + ); }, editable: false, @@ -98,7 +153,7 @@ const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { valueGetter: (params) => params.row.isContributor, renderCell: (params: GridRenderCellParams) => { return ( - + ); }, editable: false, @@ -110,7 +165,7 @@ const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { valueGetter: (params) => params.row.isTrained, renderCell: (params: GridRenderCellParams) => { return ( - + ); }, editable: false, diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 2657a2da..8e64ef5b 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -149,6 +149,9 @@ type Mutation { changeDatasetDescription(dataset: ID!, newDescription: String!): Boolean! grantProjectPermissions(project: ID!, user: ID!, isAdmin: Boolean!): Boolean! grantOwner(targetUser: ID!): Boolean! + grantStudyAdmin(study: ID!, user: ID!, isAdmin: Boolean!): Boolean! + grantContributor(study: ID!, user: ID!, isContributor: Boolean!): Boolean! + grantTrainedContributor(study: ID!, user: ID!, isTrained: Boolean!): Boolean! signLabCreateProject(project: ProjectCreate!): Project! deleteProject(project: ID!): Boolean! createStudy(study: StudyCreate!): Study! diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index 05458575..68ec6c30 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -99,4 +99,72 @@ export class PermissionService { // return permissions; return permissions; } + + async grantStudyAdmin( + study: Study, + user: string, + isAdmin: boolean, + requestingUser: TokenPayload + ): Promise { + // Make sure the target user is not a project admin + const isProjectAdmin = await this.enforcer.enforce(user, Roles.PROJECT_ADMIN, study._id); + if (isProjectAdmin) { + throw new UnauthorizedException('Target user is an owner'); + } + + // The user cannot change its own permissions + if (user === requestingUser.id) { + throw new UnauthorizedException('Cannot change your own permissions'); + } + + // Otherwise grant the permissions + if (isAdmin) { + await this.enforcer.addPolicy(user, Roles.PROJECT_ADMIN, study._id.toString()); + } else { + await this.enforcer.removePolicy(user, Roles.PROJECT_ADMIN, study._id.toString()); + } + + return true; + } + + async grantContributor( + study: Study, + user: string, + isContributor: boolean, + requestingUser: TokenPayload + ): Promise { + // Make sure the target user is not a study admin + const isStudyAdmin = await this.enforcer.enforce(user, Roles.STUDY_ADMIN, study._id.toString()); + if (isStudyAdmin) { + throw new UnauthorizedException('Target user is an owner'); + } + + // The user cannot change its own permissions + if (user === requestingUser.id) { + throw new UnauthorizedException('Cannot change your own permissions'); + } + + // Otherwise grant the permissions + if (isContributor) { + await this.enforcer.addPolicy(user, Roles.CONTRIBUTOR, study._id.toString()); + } else { + await this.enforcer.removePolicy(user, Roles.CONTRIBUTOR, study._id.toString()); + } + + return true; + } + + async grantTrainedContributor( + study: Study, + user: string, + isTrained: boolean, + _requestingUser: TokenPayload + ): Promise { + if (isTrained) { + await this.enforcer.addPolicy(user, Roles.TRAINED_CONTRIBUTOR, study._id.toString()); + } else { + await this.enforcer.removePolicy(user, Roles.TRAINED_CONTRIBUTOR, study._id.toString()); + } + return true; + } } diff --git a/packages/server/src/permission/resolvers/study.resolver.ts b/packages/server/src/permission/resolvers/study.resolver.ts index 26ca38a6..5a9557f5 100644 --- a/packages/server/src/permission/resolvers/study.resolver.ts +++ b/packages/server/src/permission/resolvers/study.resolver.ts @@ -1,4 +1,4 @@ -import { Resolver, Args, ID, Query, ResolveField, Parent } from '@nestjs/graphql'; +import { Resolver, Args, ID, Query, ResolveField, Parent, Mutation } from '@nestjs/graphql'; import { JwtAuthGuard } from '../../jwt/jwt.guard'; import { UseGuards, Inject, UnauthorizedException } from '@nestjs/common'; import { StudyPermissionModel } from '../models/study.model'; @@ -32,6 +32,54 @@ export class StudyPermissionResolver { return this.permissionService.getStudyPermissions(study, requestingUser); } + @Mutation(() => Boolean) + async grantStudyAdmin( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @Args('user', { type: () => ID }) user: string, + @Args('isAdmin', { type: () => Boolean }) isAdmin: boolean, + @TokenContext() requestingUser: TokenPayload + ): Promise { + // Make sure the user has the ability to manage study permissions + const hasPermission = await this.enforcer.enforce(requestingUser.id, StudyPermissions.GRANT_ACCESS, study._id.toString()); + if (!hasPermission) { + throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); + } + + return this.permissionService.grantStudyAdmin(study, user, isAdmin, requestingUser); + } + + @Mutation(() => Boolean) + async grantContributor( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @Args('user', { type: () => ID }) user: string, + @Args('isContributor', { type: () => Boolean }) isContributor: boolean, + @TokenContext() requestingUser: TokenPayload + ): Promise { + // Make sure the user has the ability to manage study permissions + const hasPermission = await this.enforcer.enforce(requestingUser.id, StudyPermissions.GRANT_ACCESS, study._id.toString()); + if (!hasPermission) { + throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); + } + + return this.permissionService.grantContributor(study, user, isContributor, requestingUser); + } + + @Mutation(() => Boolean) + async grantTrainedContributor( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @Args('user', { type: () => ID }) user: string, + @Args('isTrained', { type: () => Boolean }) isTrained: boolean, + @TokenContext() requestingUser: TokenPayload + ): Promise { + // Make sure the user has the ability to manage study permissions + const hasPermission = await this.enforcer.enforce(requestingUser.id, StudyPermissions.GRANT_ACCESS, study._id.toString()); + if (!hasPermission) { + throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); + } + + return this.permissionService.grantTrainedContributor(study, user, isTrained, requestingUser); + } + @ResolveField('user', () => UserModel) resolveUser(@Parent() permission: StudyPermissionModel): any { return { __typename: 'UserModel', id: permission.user }; From d0b9bc71e8aa32886e1fe6415880257aaa5677ae Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 17:08:30 -0500 Subject: [PATCH 09/10] Working study permission control --- packages/server/src/permission/permission.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index 68ec6c30..b9e8b988 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -78,6 +78,7 @@ export class PermissionService { users.map(async (user) => { const isStudyAdmin = await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString()); const isStudyAdminEditable = !(await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, study._id.toString())); + console.log(user, isStudyAdminEditable); const isContributor = await this.enforcer.enforce(user.id, Roles.CONTRIBUTOR, study._id.toString()); const isContributorEditable = !(await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString())); @@ -119,9 +120,9 @@ export class PermissionService { // Otherwise grant the permissions if (isAdmin) { - await this.enforcer.addPolicy(user, Roles.PROJECT_ADMIN, study._id.toString()); + await this.enforcer.addPolicy(user, Roles.STUDY_ADMIN, study._id.toString()); } else { - await this.enforcer.removePolicy(user, Roles.PROJECT_ADMIN, study._id.toString()); + await this.enforcer.removePolicy(user, Roles.STUDY_ADMIN, study._id.toString()); } return true; From f0ace64c9cdaf8000d6c80a354e929b345c35d3f Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 17:08:51 -0500 Subject: [PATCH 10/10] Fix formatting --- .../src/pages/studies/UserPermissions.tsx | 9 ++++--- .../src/permission/models/project.model.ts | 1 - .../src/permission/models/study.model.ts | 1 - .../src/permission/permission.module.ts | 8 ++++++- .../src/permission/permission.service.ts | 7 +----- .../permission/resolvers/project.resolver.ts | 6 +++-- .../permission/resolvers/study.resolver.ts | 24 +++++++++++++++---- 7 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/client/src/pages/studies/UserPermissions.tsx b/packages/client/src/pages/studies/UserPermissions.tsx index b7ffa4cc..f752d4cc 100644 --- a/packages/client/src/pages/studies/UserPermissions.tsx +++ b/packages/client/src/pages/studies/UserPermissions.tsx @@ -3,10 +3,14 @@ import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { useStudy } from '../../context/Study.context'; import { Study, StudyPermissionModel } from '../../graphql/graphql'; import { DecodedToken, useAuth } from '../../context/Auth.context'; -import { useGetStudyPermissionsQuery, useGrantStudyAdminMutation, useGrantContributorMutation, useGrantTrainedContributorMutation } from '../../graphql/permission/permission'; +import { + useGetStudyPermissionsQuery, + useGrantStudyAdminMutation, + useGrantContributorMutation, + useGrantTrainedContributorMutation +} from '../../graphql/permission/permission'; import { useEffect, useState } from 'react'; - export const StudyUserPermissions: React.FC = () => { const { study } = useStudy(); @@ -109,7 +113,6 @@ const EditTrainedSwitch: React.FC = (props) => { ); }; - const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { const { decodedToken } = useAuth(); const { data, refetch } = useGetStudyPermissionsQuery({ diff --git a/packages/server/src/permission/models/project.model.ts b/packages/server/src/permission/models/project.model.ts index 8cd31966..b528928a 100644 --- a/packages/server/src/permission/models/project.model.ts +++ b/packages/server/src/permission/models/project.model.ts @@ -1,7 +1,6 @@ import { ObjectType, Field } from '@nestjs/graphql'; import { UserModel } from '../../auth/user.model'; - @ObjectType() export class ProjectPermissionModel { @Field(() => UserModel) diff --git a/packages/server/src/permission/models/study.model.ts b/packages/server/src/permission/models/study.model.ts index ebe889d8..9b5e73ab 100644 --- a/packages/server/src/permission/models/study.model.ts +++ b/packages/server/src/permission/models/study.model.ts @@ -1,7 +1,6 @@ import { ObjectType, Field } from '@nestjs/graphql'; import { UserModel } from '../../auth/user.model'; - @ObjectType() export class StudyPermissionModel { @Field(() => UserModel) diff --git a/packages/server/src/permission/permission.module.ts b/packages/server/src/permission/permission.module.ts index a3eadad0..8d4915a2 100644 --- a/packages/server/src/permission/permission.module.ts +++ b/packages/server/src/permission/permission.module.ts @@ -10,7 +10,13 @@ import { StudyPermissionResolver } from './resolvers/study.resolver'; @Module({ imports: [forwardRef(() => ProjectModule), AuthModule, forwardRef(() => StudyModule)], - providers: [casbinProvider, PermissionService, ProjectPermissionResolver, OwnerPermissionResolver, StudyPermissionResolver], + providers: [ + casbinProvider, + PermissionService, + ProjectPermissionResolver, + OwnerPermissionResolver, + StudyPermissionResolver + ], exports: [casbinProvider] }) export class PermissionModule {} diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index b9e8b988..3792ad0f 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -101,12 +101,7 @@ export class PermissionService { return permissions; } - async grantStudyAdmin( - study: Study, - user: string, - isAdmin: boolean, - requestingUser: TokenPayload - ): Promise { + async grantStudyAdmin(study: Study, user: string, isAdmin: boolean, requestingUser: TokenPayload): Promise { // Make sure the target user is not a project admin const isProjectAdmin = await this.enforcer.enforce(user, Roles.PROJECT_ADMIN, study._id); if (isProjectAdmin) { diff --git a/packages/server/src/permission/resolvers/project.resolver.ts b/packages/server/src/permission/resolvers/project.resolver.ts index 20a3454f..3fd50710 100644 --- a/packages/server/src/permission/resolvers/project.resolver.ts +++ b/packages/server/src/permission/resolvers/project.resolver.ts @@ -15,8 +15,10 @@ import { UserModel } from '../../auth/user.model'; @UseGuards(JwtAuthGuard) @Resolver(() => ProjectPermissionModel) export class ProjectPermissionResolver { - constructor(@Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, - private readonly permissionService: PermissionService) {} + constructor( + @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, + private readonly permissionService: PermissionService + ) {} @Query(() => [ProjectPermissionModel]) async getProjectPermissions( diff --git a/packages/server/src/permission/resolvers/study.resolver.ts b/packages/server/src/permission/resolvers/study.resolver.ts index 5a9557f5..39f3a64e 100644 --- a/packages/server/src/permission/resolvers/study.resolver.ts +++ b/packages/server/src/permission/resolvers/study.resolver.ts @@ -15,8 +15,10 @@ import { UserModel } from '../../auth/user.model'; @UseGuards(JwtAuthGuard) @Resolver(() => StudyPermissionModel) export class StudyPermissionResolver { - constructor(@Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, - private readonly permissionService: PermissionService) {} + constructor( + @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, + private readonly permissionService: PermissionService + ) {} @Query(() => [StudyPermissionModel]) async getStudyPermissions( @@ -40,7 +42,11 @@ export class StudyPermissionResolver { @TokenContext() requestingUser: TokenPayload ): Promise { // Make sure the user has the ability to manage study permissions - const hasPermission = await this.enforcer.enforce(requestingUser.id, StudyPermissions.GRANT_ACCESS, study._id.toString()); + const hasPermission = await this.enforcer.enforce( + requestingUser.id, + StudyPermissions.GRANT_ACCESS, + study._id.toString() + ); if (!hasPermission) { throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); } @@ -56,7 +62,11 @@ export class StudyPermissionResolver { @TokenContext() requestingUser: TokenPayload ): Promise { // Make sure the user has the ability to manage study permissions - const hasPermission = await this.enforcer.enforce(requestingUser.id, StudyPermissions.GRANT_ACCESS, study._id.toString()); + const hasPermission = await this.enforcer.enforce( + requestingUser.id, + StudyPermissions.GRANT_ACCESS, + study._id.toString() + ); if (!hasPermission) { throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); } @@ -72,7 +82,11 @@ export class StudyPermissionResolver { @TokenContext() requestingUser: TokenPayload ): Promise { // Make sure the user has the ability to manage study permissions - const hasPermission = await this.enforcer.enforce(requestingUser.id, StudyPermissions.GRANT_ACCESS, study._id.toString()); + const hasPermission = await this.enforcer.enforce( + requestingUser.id, + StudyPermissions.GRANT_ACCESS, + study._id.toString() + ); if (!hasPermission) { throw new UnauthorizedException('Requesting user does not have permission to manage study permissions'); }