From 387ddc8b1279ea485f3a07a41a2e184524837a33 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 11:14:28 -0500 Subject: [PATCH 1/6] Add role query to client --- packages/client/src/graphql/graphql.ts | 21 +++++++ .../src/graphql/permission/permission.graphql | 12 ++++ .../src/graphql/permission/permission.ts | 57 +++++++++++++++++++ .../src/permission/permission.resolver.ts | 29 ++++++++-- .../src/permission/permission.service.ts | 6 -- .../src/permission/permissions/project.ts | 2 +- 6 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 packages/client/src/graphql/permission/permission.graphql create mode 100644 packages/client/src/graphql/permission/permission.ts diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 1f99c876..4a38402d 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -409,6 +409,14 @@ 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']; @@ -485,6 +493,7 @@ export type Query = { getEntryUploadURL: Scalars['String']['output']; getOrganizations: Array; getProject: ProjectModel; + getProjectPermissions: Array; getProjects: Array; getUser: UserModel; invite: InviteModel; @@ -535,6 +544,11 @@ export type QueryGetProjectArgs = { }; +export type QueryGetProjectPermissionsArgs = { + project: Scalars['ID']['input']; +}; + + export type QueryGetUserArgs = { id: Scalars['ID']['input']; }; @@ -589,6 +603,13 @@ 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 new file mode 100644 index 00000000..7d08de80 --- /dev/null +++ b/packages/client/src/graphql/permission/permission.graphql @@ -0,0 +1,12 @@ +query getProjectPermissions($project: ID!) { + getProjectPermissions(project: $project) { + user { + id, + fullname, + username, + email + }, + hasRole, + editable + } +} diff --git a/packages/client/src/graphql/permission/permission.ts b/packages/client/src/graphql/permission/permission.ts new file mode 100644 index 00000000..06d20628 --- /dev/null +++ b/packages/client/src/graphql/permission/permission.ts @@ -0,0 +1,57 @@ +/* Generated File DO NOT EDIT. */ +/* tslint:disable */ +import * as Types from '../graphql'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type GetProjectPermissionsQueryVariables = Types.Exact<{ + project: Types.Scalars['ID']['input']; +}>; + + +export type GetProjectPermissionsQuery = { __typename?: 'Query', getProjectPermissions: Array<{ __typename?: 'Permission', hasRole: boolean, editable: boolean, user: { __typename?: 'UserModel', id: string, fullname?: string | null, username?: string | null, email?: string | null } }> }; + + +export const GetProjectPermissionsDocument = gql` + query getProjectPermissions($project: ID!) { + getProjectPermissions(project: $project) { + user { + id + fullname + username + email + } + hasRole + editable + } +} + `; + +/** + * __useGetProjectPermissionsQuery__ + * + * To run a query within a React component, call `useGetProjectPermissionsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetProjectPermissionsQuery` 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 } = useGetProjectPermissionsQuery({ + * variables: { + * project: // value for 'project' + * }, + * }); + */ +export function useGetProjectPermissionsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetProjectPermissionsDocument, options); + } +export function useGetProjectPermissionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetProjectPermissionsDocument, options); + } +export type GetProjectPermissionsQueryHookResult = ReturnType; +export type GetProjectPermissionsLazyQueryHookResult = ReturnType; +export type GetProjectPermissionsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/packages/server/src/permission/permission.resolver.ts b/packages/server/src/permission/permission.resolver.ts index 7204f3ae..4c86ab87 100644 --- a/packages/server/src/permission/permission.resolver.ts +++ b/packages/server/src/permission/permission.resolver.ts @@ -1,19 +1,26 @@ import { Resolver, Mutation, Args, ID, Query, ResolveField, Parent } from '@nestjs/graphql'; import { JwtAuthGuard } from '../jwt/jwt.guard'; -import { UseGuards } from '@nestjs/common'; +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 'src/organization/organization.context'; -import { Organization } from 'src/organization/organization.model'; +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'; @UseGuards(JwtAuthGuard) @Resolver(() => Permission) export class PermissionResolver { - constructor(private readonly permissionService: PermissionService) {} + constructor( + private readonly permissionService: PermissionService, + @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer + ) {} @Mutation(() => Boolean) async grantOwner( @@ -21,6 +28,12 @@ export class PermissionResolver { @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, requestingUser.id, organization._id); return true; } @@ -30,6 +43,14 @@ export class PermissionResolver { @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); } diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index 3e49d3d8..a0d999cd 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -16,12 +16,6 @@ export class PermissionService { /** requestingUser must be an owner themselves */ async grantOwner(targetUser: string, requestingUser: string, organization: string): 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.enforcer.addPolicy(targetUser, Roles.OWNER, organization); } diff --git a/packages/server/src/permission/permissions/project.ts b/packages/server/src/permission/permissions/project.ts index 0d055daf..3fa625b1 100644 --- a/packages/server/src/permission/permissions/project.ts +++ b/packages/server/src/permission/permissions/project.ts @@ -14,10 +14,10 @@ export const roleToProjectPermissions: string[][] = [ // OWNER permissions [Roles.OWNER, ProjectPermissions.CREATE], [Roles.OWNER, ProjectPermissions.DELETE], - [Roles.OWNER, ProjectPermissions.GRANT_ADMIN], // PROJECT_ADMIN permissions [Roles.PROJECT_ADMIN, ProjectPermissions.UPDATE], + [Roles.OWNER, ProjectPermissions.GRANT_ADMIN], // STUDY_ADMIN permissions From e39111b78c491149c6b5d601a8407460684e04b3 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 11:45:33 -0500 Subject: [PATCH 2/6] Rendering of permissions correctly --- .../src/graphql/permission/permission.graphql | 10 +- .../src/graphql/permission/permission.ts | 8 +- .../pages/projects/ProjectUserPermissions.tsx | 127 ++++++++---------- 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/packages/client/src/graphql/permission/permission.graphql b/packages/client/src/graphql/permission/permission.graphql index 7d08de80..929a4060 100644 --- a/packages/client/src/graphql/permission/permission.graphql +++ b/packages/client/src/graphql/permission/permission.graphql @@ -2,11 +2,17 @@ query getProjectPermissions($project: ID!) { getProjectPermissions(project: $project) { user { id, + projectId, fullname, username, - email + email, + role, + createdAt, + updatedAt, + deletedAt }, hasRole, - editable + editable, + role } } diff --git a/packages/client/src/graphql/permission/permission.ts b/packages/client/src/graphql/permission/permission.ts index 06d20628..76e223e3 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, user: { __typename?: 'UserModel', id: string, fullname?: string | null, username?: string | null, email?: string | null } }> }; +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 const GetProjectPermissionsDocument = gql` @@ -18,12 +18,18 @@ export const GetProjectPermissionsDocument = gql` getProjectPermissions(project: $project) { user { id + projectId fullname username email + role + createdAt + updatedAt + deletedAt } hasRole editable + role } } `; diff --git a/packages/client/src/pages/projects/ProjectUserPermissions.tsx b/packages/client/src/pages/projects/ProjectUserPermissions.tsx index 368ab392..03c2f15b 100644 --- a/packages/client/src/pages/projects/ProjectUserPermissions.tsx +++ b/packages/client/src/pages/projects/ProjectUserPermissions.tsx @@ -1,107 +1,98 @@ 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 { useEffect, useState } from 'react'; +import { useProject } from '../../context/Project.context'; +import { Permission, Project } from '../../graphql/graphql'; +import { useGetProjectPermissionsQuery } from '../../graphql/permission/permission'; -const SwitchEditInputCell: React.FC = (props: GridRenderCellParams) => { - const { id, value, field, hasFocus } = props; - const apiRef = useGridApiContext(); - const ref = useRef(); +export const ProjectUserPermissions: React.FC = () => { + const { project } = useProject(); + + return ( + <> + User Permissions + {project && } + + ); +}; + +interface EditAdminSwitchProps { + permission: Permission; +} + +const EditAdminSwitch: React.FC = (props) => { + const handleChange = () => { - const handleChange = (newValue: boolean | false) => { - apiRef.current.setEditCellValue({ id, field, value: newValue }); }; - useEnhancedEffect(() => { - if (hasFocus && ref.current) { - const input = ref.current.querySelector(`input[value="${value}"]`); - input?.focus(); - } - }, [hasFocus, value]); + console.log(props.permission.hasRole); - return handleChange} />; + + return ( + handleChange()} + disabled={!props.permission.editable} + /> + ); }; -const tableRows = [ - { - id: 1, - name: 'Professor Flour', - username: 'flour123', - email: 'bread@bread.com', - switch: true - }, - { - id: 2, - name: 'Elon', - username: 'elon1012', - email: 'elonmusk@hotmail.com', - switch: true - }, - { - id: 3, - name: 'Chrishell Stausse', - username: 'chrishell123', - email: 'chrishell@gmail.com', - switch: false - }, - { - id: 4, - name: 'Project Charles', - username: 'charlie', - email: '111@hotmail.com', - switch: false - } -]; +const UserPermissionTable: React.FC<{ project: Project }> = ({ project }) => { + const { data } = useGetProjectPermissionsQuery({ + variables: { + project: project._id + } + }); -export const ProjectUserPermissions: React.FC = () => { - const [rows] = useState(tableRows); - const [rowModesModel, setRowModesModel] = useState({}); + const [rows, setRows] = useState([]); - const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - setRowModesModel(newRowModesModel); - }; + useEffect(() => { + if (data?.getProjectPermissions) { + setRows(data.getProjectPermissions); + } + }, [data]); const columns: GridColDef[] = [ - { field: 'id', headerName: 'ID', flex: 0.5 }, + /* For now, only email is populated, this will change in the future { field: 'name', headerName: 'Name', + valueGetter: (params) => params.row.user.fullname, flex: 1.25, - editable: true + editable: false, }, { field: 'username', headerName: 'Username', + valueGetter: (params) => params.row.user.username, flex: 1.75, - editable: true + editable: false }, + */ { field: 'email', headerName: 'Email', + valueGetter: (params) => params.row.user.email, flex: 1.75, - editable: true + editable: false }, { - field: 'switch', + field: 'projectAdmin', type: 'boolean', - headerName: 'Switch', - renderCell: (params) => , - renderEditCell: (params) => , - editable: true, + headerName: 'Project Admin', + valueGetter: (params) => params.row.hasRole, + renderCell: (params: GridRenderCellParams) => , + editable: false, flex: 1 } ]; return ( - <> - User Permissions - 'auto'} rows={rows} columns={columns} - rowModesModel={rowModesModel} - onRowModesModelChange={handleRowModesModelChange} + getRowId={(row) => row.user.id} initialState={{ pagination: { paginationModel: { @@ -110,9 +101,7 @@ export const ProjectUserPermissions: React.FC = () => { } }} pageSizeOptions={[5]} - checkboxSelection disableRowSelectionOnClick /> - ); }; From 982293fd76e7a94539b4ae73d8be75afb922982c Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 11:56:51 -0500 Subject: [PATCH 3/6] Add ability to change project admin status to backend --- packages/server/schema.gql | 1 + .../src/permission/permission.resolver.ts | 17 ++++++++++++++ .../src/permission/permission.service.ts | 22 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 1bbaeea9..2894f351 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -145,6 +145,7 @@ type Mutation { changeDatasetName(dataset: ID!, newName: String!): Boolean! changeDatasetDescription(dataset: ID!, newDescription: String!): Boolean! grantOwner(targetUser: ID!): Boolean! + grantProjectPermissions(project: ID!, user: ID!, isAdmin: Boolean!): Boolean! signLabCreateProject(project: ProjectCreate!): Project! deleteProject(project: ID!): Boolean! createStudy(study: StudyCreate!): Study! diff --git a/packages/server/src/permission/permission.resolver.ts b/packages/server/src/permission/permission.resolver.ts index 4c86ab87..fe2fee60 100644 --- a/packages/server/src/permission/permission.resolver.ts +++ b/packages/server/src/permission/permission.resolver.ts @@ -54,6 +54,23 @@ export class PermissionResolver { 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: 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 a0d999cd..f54aebbc 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -40,4 +40,26 @@ export class PermissionService { return permissions; } + + async grantProjectPermissions(project: Project, user: string, isAdmin: boolean, requestingUser: TokenPayload): Promise { + // Make sure the target user is not an owner + const isOwner = await this.enforcer.enforce(user, Roles.OWNER, project._id); + if (isOwner) { + 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, project._id); + } else { + await this.enforcer.removePolicy(user, Roles.PROJECT_ADMIN, project._id); + } + + return true; + } } From d23f77fa9ab1c1ec010f7b5f7c2994482412608b Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 12:21:24 -0500 Subject: [PATCH 4/6] Able to add policy, cannot remove policy --- packages/client/src/graphql/graphql.ts | 8 ++++ .../src/graphql/permission/permission.graphql | 4 ++ .../src/graphql/permission/permission.ts | 44 ++++++++++++++++++- .../pages/projects/ProjectUserPermissions.tsx | 43 ++++++++++++++---- .../src/permission/permission.service.ts | 7 ++- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 4a38402d..1b13dfd2 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -190,6 +190,7 @@ export type Mutation = { deleteStudy: Scalars['Boolean']['output']; forgotPassword: Scalars['Boolean']['output']; grantOwner: Scalars['Boolean']['output']; + grantProjectPermissions: Scalars['Boolean']['output']; lexiconAddEntry: LexiconEntry; /** Remove all entries from a given lexicon */ lexiconClearEntries: Scalars['Boolean']['output']; @@ -316,6 +317,13 @@ export type MutationGrantOwnerArgs = { }; +export type MutationGrantProjectPermissionsArgs = { + isAdmin: Scalars['Boolean']['input']; + project: 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 929a4060..7b838616 100644 --- a/packages/client/src/graphql/permission/permission.graphql +++ b/packages/client/src/graphql/permission/permission.graphql @@ -16,3 +16,7 @@ query getProjectPermissions($project: ID!) { role } } + +mutation grantProjectPermissions($project: ID!, $user: ID!, $isAdmin: Boolean!) { + grantProjectPermissions(project: $project, user: $user, isAdmin: $isAdmin) +} diff --git a/packages/client/src/graphql/permission/permission.ts b/packages/client/src/graphql/permission/permission.ts index 76e223e3..17ebf1d9 100644 --- a/packages/client/src/graphql/permission/permission.ts +++ b/packages/client/src/graphql/permission/permission.ts @@ -12,6 +12,15 @@ 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 GrantProjectPermissionsMutationVariables = Types.Exact<{ + project: Types.Scalars['ID']['input']; + user: Types.Scalars['ID']['input']; + isAdmin: Types.Scalars['Boolean']['input']; +}>; + + +export type GrantProjectPermissionsMutation = { __typename?: 'Mutation', grantProjectPermissions: boolean }; + export const GetProjectPermissionsDocument = gql` query getProjectPermissions($project: ID!) { @@ -60,4 +69,37 @@ export function useGetProjectPermissionsLazyQuery(baseOptions?: Apollo.LazyQuery } export type GetProjectPermissionsQueryHookResult = ReturnType; export type GetProjectPermissionsLazyQueryHookResult = ReturnType; -export type GetProjectPermissionsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetProjectPermissionsQueryResult = Apollo.QueryResult; +export const GrantProjectPermissionsDocument = gql` + mutation grantProjectPermissions($project: ID!, $user: ID!, $isAdmin: Boolean!) { + grantProjectPermissions(project: $project, user: $user, isAdmin: $isAdmin) +} + `; +export type GrantProjectPermissionsMutationFn = Apollo.MutationFunction; + +/** + * __useGrantProjectPermissionsMutation__ + * + * To run a mutation, you first call `useGrantProjectPermissionsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGrantProjectPermissionsMutation` 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 [grantProjectPermissionsMutation, { data, loading, error }] = useGrantProjectPermissionsMutation({ + * variables: { + * project: // value for 'project' + * user: // value for 'user' + * isAdmin: // value for 'isAdmin' + * }, + * }); + */ +export function useGrantProjectPermissionsMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GrantProjectPermissionsDocument, options); + } +export type GrantProjectPermissionsMutationHookResult = ReturnType; +export type GrantProjectPermissionsMutationResult = Apollo.MutationResult; +export type GrantProjectPermissionsMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file diff --git a/packages/client/src/pages/projects/ProjectUserPermissions.tsx b/packages/client/src/pages/projects/ProjectUserPermissions.tsx index 03c2f15b..966f7c18 100644 --- a/packages/client/src/pages/projects/ProjectUserPermissions.tsx +++ b/packages/client/src/pages/projects/ProjectUserPermissions.tsx @@ -1,9 +1,11 @@ import { Switch, Typography } from '@mui/material'; import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { useEffect, useState } from 'react'; +import { ChangeEvent, useEffect, useState } from 'react'; import { useProject } from '../../context/Project.context'; import { Permission, Project } from '../../graphql/graphql'; import { useGetProjectPermissionsQuery } from '../../graphql/permission/permission'; +import { DecodedToken, useAuth } from '../../context/Auth.context'; +import { useGrantProjectPermissionsMutation } from '../../graphql/permission/permission'; export const ProjectUserPermissions: React.FC = () => { const { project } = useProject(); @@ -18,33 +20,49 @@ export const ProjectUserPermissions: React.FC = () => { interface EditAdminSwitchProps { permission: Permission; + currentUser: DecodedToken; + project: Project; + refetch: () => void; } const EditAdminSwitch: React.FC = (props) => { - const handleChange = () => { - }; +const [grantProjectPermissions, grantProjectPermissionsResults] = useGrantProjectPermissionsMutation(); - console.log(props.permission.hasRole); + const handleChange = (event: ChangeEvent) => { + grantProjectPermissions({ + variables: { + project: props.project._id, + user: props.permission.user.id, + isAdmin: event.target.checked + } + }); + }; + useEffect(() => { + if (grantProjectPermissionsResults.data) { + props.refetch(); + } + }, [grantProjectPermissionsResults]); return ( handleChange()} - disabled={!props.permission.editable} + onChange={handleChange} + disabled={!props.permission.editable || props.permission.user.id === props.currentUser.id} /> ); }; const UserPermissionTable: React.FC<{ project: Project }> = ({ project }) => { - const { data } = useGetProjectPermissionsQuery({ + const { data, refetch } = useGetProjectPermissionsQuery({ variables: { project: project._id } }); const [rows, setRows] = useState([]); + const { decodedToken } = useAuth(); useEffect(() => { if (data?.getProjectPermissions) { @@ -81,7 +99,16 @@ const UserPermissionTable: React.FC<{ project: Project }> = ({ project }) => { type: 'boolean', headerName: 'Project Admin', valueGetter: (params) => params.row.hasRole, - renderCell: (params: GridRenderCellParams) => , + renderCell: (params: GridRenderCellParams) => { + return ( + + ) + }, editable: false, flex: 1 } diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index f54aebbc..f8962d28 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -53,11 +53,16 @@ export class PermissionService { throw new UnauthorizedException('Cannot change your own permissions'); } + console.log(project); + console.log(user); + // Otherwise grant the permissions if (isAdmin) { await this.enforcer.addPolicy(user, Roles.PROJECT_ADMIN, project._id); } else { - await this.enforcer.removePolicy(user, Roles.PROJECT_ADMIN, project._id); + console.log('Has policy: ', await this.enforcer.enforce(user, Roles.PROJECT_ADMIN, project._id)); + const result = await this.enforcer.removePolicy(user, Roles.PROJECT_ADMIN, project._id); + // console.log(result); } return true; From 06690b83877bbfcc0684cc3920c1075c5f06ea06 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 12:30:33 -0500 Subject: [PATCH 5/6] Working removal of permission --- packages/server/src/permission/permission.service.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index f8962d28..27281364 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -53,16 +53,11 @@ export class PermissionService { throw new UnauthorizedException('Cannot change your own permissions'); } - console.log(project); - console.log(user); - // Otherwise grant the permissions if (isAdmin) { - await this.enforcer.addPolicy(user, Roles.PROJECT_ADMIN, project._id); + await this.enforcer.addPolicy(user, Roles.PROJECT_ADMIN, project._id.toString()); } else { - console.log('Has policy: ', await this.enforcer.enforce(user, Roles.PROJECT_ADMIN, project._id)); - const result = await this.enforcer.removePolicy(user, Roles.PROJECT_ADMIN, project._id); - // console.log(result); + await this.enforcer.removePolicy(user, Roles.PROJECT_ADMIN, project._id.toString()); } return true; From eed98069df2b360459967a74894f91020eabc5cc Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 5 Jan 2024 12:31:06 -0500 Subject: [PATCH 6/6] Fix formatting --- .../pages/projects/ProjectUserPermissions.tsx | 38 ++++++++----------- .../src/permission/permission.resolver.ts | 10 ++--- .../src/permission/permission.service.ts | 7 +++- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/packages/client/src/pages/projects/ProjectUserPermissions.tsx b/packages/client/src/pages/projects/ProjectUserPermissions.tsx index 966f7c18..05b4f2ba 100644 --- a/packages/client/src/pages/projects/ProjectUserPermissions.tsx +++ b/packages/client/src/pages/projects/ProjectUserPermissions.tsx @@ -26,8 +26,7 @@ interface EditAdminSwitchProps { } const EditAdminSwitch: React.FC = (props) => { - -const [grantProjectPermissions, grantProjectPermissionsResults] = useGrantProjectPermissionsMutation(); + const [grantProjectPermissions, grantProjectPermissionsResults] = useGrantProjectPermissionsMutation(); const handleChange = (event: ChangeEvent) => { grantProjectPermissions({ @@ -101,13 +100,8 @@ const UserPermissionTable: React.FC<{ project: Project }> = ({ project }) => { valueGetter: (params) => params.row.hasRole, renderCell: (params: GridRenderCellParams) => { return ( - - ) + + ); }, editable: false, flex: 1 @@ -116,19 +110,19 @@ const UserPermissionTable: React.FC<{ project: Project }> = ({ project }) => { return ( 'auto'} - rows={rows} - columns={columns} - getRowId={(row) => row.user.id} - initialState={{ - pagination: { - paginationModel: { - pageSize: 5 - } + getRowHeight={() => 'auto'} + rows={rows} + columns={columns} + getRowId={(row) => row.user.id} + initialState={{ + pagination: { + paginationModel: { + pageSize: 5 } - }} - pageSizeOptions={[5]} - disableRowSelectionOnClick - /> + } + }} + pageSizeOptions={[5]} + disableRowSelectionOnClick + /> ); }; diff --git a/packages/server/src/permission/permission.resolver.ts b/packages/server/src/permission/permission.resolver.ts index fe2fee60..734bad19 100644 --- a/packages/server/src/permission/permission.resolver.ts +++ b/packages/server/src/permission/permission.resolver.ts @@ -44,9 +44,7 @@ export class PermissionResolver { @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); + 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'); } @@ -59,11 +57,9 @@ export class PermissionResolver { @Args('project', { type: () => ID }, ProjectPipe) project: Project, @Args('user', { type: () => ID }) user: string, @Args('isAdmin', { type: () => Boolean }) isAdmin: boolean, - @TokenContext() requestingUser: TokenPayload, + @TokenContext() requestingUser: TokenPayload ): Promise { - const hasPermission = await this.enforcer.enforce(requestingUser.id, - ProjectPermissions.GRANT_ADMIN, - project._id); + 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'); } diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index 27281364..408ce965 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -41,7 +41,12 @@ export class PermissionService { return permissions; } - async grantProjectPermissions(project: Project, user: string, isAdmin: boolean, requestingUser: TokenPayload): Promise { + async grantProjectPermissions( + project: Project, + user: string, + isAdmin: boolean, + requestingUser: TokenPayload + ): Promise { // Make sure the target user is not an owner const isOwner = await this.enforcer.enforce(user, Roles.OWNER, project._id); if (isOwner) {