From 35cb0e84dd02d868862aff222fe62023b74fbb10 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 22 Apr 2021 09:17:45 +0300 Subject: [PATCH 01/29] Roles supporting backend Signed-off-by: Alexander Ivanov --- .../models/events/enums/EventType.prisma | 8 +++ packages/core/prisma/models/roles/Role.prisma | 15 +++++ packages/core/prisma/models/users/User.prisma | 3 + packages/core/prisma/schema.prisma | 27 +++++++++ packages/core/src/domain/validation/index.ts | 9 ++- packages/core/src/index.ts | 4 +- .../core/src/services/authorization/index.ts | 6 +- .../src/services/authorization/userCan.ts | 30 ++++++++++ packages/core/src/services/index.ts | 1 + .../roles/commands/createRoleCommand.ts | 42 ++++++++++++++ .../permissions/addRolePermissionCommand.ts | 0 .../removeRolePermissionCommand.ts | 0 .../commands/users/addUserToRoleCommand.ts | 55 ++++++++++++++++++ .../users/rebuildUserPermissionsCommand.ts | 56 +++++++++++++++++++ .../users/removeUserFromRoleCommand.ts | 31 ++++++++++ packages/core/src/services/roles/index.ts | 30 ++++++++++ 16 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 packages/core/prisma/models/roles/Role.prisma create mode 100644 packages/core/src/services/authorization/userCan.ts create mode 100644 packages/core/src/services/roles/commands/createRoleCommand.ts create mode 100644 packages/core/src/services/roles/commands/permissions/addRolePermissionCommand.ts create mode 100644 packages/core/src/services/roles/commands/permissions/removeRolePermissionCommand.ts create mode 100644 packages/core/src/services/roles/commands/users/addUserToRoleCommand.ts create mode 100644 packages/core/src/services/roles/commands/users/rebuildUserPermissionsCommand.ts create mode 100644 packages/core/src/services/roles/commands/users/removeUserFromRoleCommand.ts create mode 100644 packages/core/src/services/roles/index.ts diff --git a/packages/core/prisma/models/events/enums/EventType.prisma b/packages/core/prisma/models/events/enums/EventType.prisma index f7d0a2f44..2a6fdcc66 100644 --- a/packages/core/prisma/models/events/enums/EventType.prisma +++ b/packages/core/prisma/models/events/enums/EventType.prisma @@ -46,4 +46,12 @@ enum EventType { ReportRespected ReportDismissed + + RoleCreated + RolePermissionAdded + RolePermissionRemoved + RoleDeleted + + UserAddedToRole + UserRemovedFromRole } \ No newline at end of file diff --git a/packages/core/prisma/models/roles/Role.prisma b/packages/core/prisma/models/roles/Role.prisma new file mode 100644 index 000000000..01aa23ed5 --- /dev/null +++ b/packages/core/prisma/models/roles/Role.prisma @@ -0,0 +1,15 @@ +model Role { + id String @id @default(uuid()) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + name String @unique + displayName String + + description String + + permissions String[] + + users User[] +} diff --git a/packages/core/prisma/models/users/User.prisma b/packages/core/prisma/models/users/User.prisma index db98d2f57..1099adec4 100644 --- a/packages/core/prisma/models/users/User.prisma +++ b/packages/core/prisma/models/users/User.prisma @@ -29,4 +29,7 @@ model User { notificationLanguage NotificationLanguage @default(EN) notificationTokens UserNotificationToken[] + + roles Role[] + permissions String[] } \ No newline at end of file diff --git a/packages/core/prisma/schema.prisma b/packages/core/prisma/schema.prisma index 3c8a4c977..dc1242c3a 100644 --- a/packages/core/prisma/schema.prisma +++ b/packages/core/prisma/schema.prisma @@ -151,6 +151,22 @@ model CommonMember { @@unique([userId, commonId]) } +model Role { + id String @id @default(uuid()) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + name String @unique + displayName String + + description String + + permissions String[] + + users User[] +} + enum SubscriptionStatus { Pending @@ -340,6 +356,9 @@ model User { notificationLanguage NotificationLanguage @default(EN) notificationTokens UserNotificationToken[] + + roles Role[] + permissions String[] } enum UserNotificationTokenState { @@ -413,6 +432,14 @@ enum EventType { ReportRespected ReportDismissed + + RoleCreated + RolePermissionAdded + RolePermissionRemoved + RoleDeleted + + UserAddedToRole + UserRemovedFromRole } model Event { diff --git a/packages/core/src/domain/validation/index.ts b/packages/core/src/domain/validation/index.ts index 8ce30a507..bbb5cc7d2 100644 --- a/packages/core/src/domain/validation/index.ts +++ b/packages/core/src/domain/validation/index.ts @@ -1,4 +1,11 @@ +import * as z from 'zod'; + export { ProposalLinkSchema } from './schemas/ProposalLink.schema'; export { BillingDetailsSchema } from './schemas/BillingDetails.schema'; export { ProposalImageSchema } from './schemas/ProposalImage.schema'; -export { ProposalFileSchema } from './schemas/ProposalFile.schema'; \ No newline at end of file +export { ProposalFileSchema } from './schemas/ProposalFile.schema'; + +export const PermissionValidator = z.enum([ + 'admin.report.read', + 'admin.report.act' +]); \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2f7028527..e95b7ad45 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,14 +4,14 @@ import { FirebaseToolkit, SendgridToolkit } from '@toolkits'; FirebaseToolkit.InitializeFirebase(); SendgridToolkit.InitializeSendgrid(); -export { prisma, pubSub } from '@toolkits'; export { logger } from '@logger'; +export { prisma, pubSub } from '@toolkits'; // Domain export { CommonError } from '@errors'; - // Services +export { roleService } from './services/roles'; export { cardService } from './services/cards'; export { voteService } from './services/votes'; export { userService } from './services/users'; diff --git a/packages/core/src/services/authorization/index.ts b/packages/core/src/services/authorization/index.ts index 5595a4127..3b157a10e 100644 --- a/packages/core/src/services/authorization/index.ts +++ b/packages/core/src/services/authorization/index.ts @@ -3,6 +3,8 @@ import { canSeeCommonReports } from './reports/canSeeCommonReports'; import { canSeeMessageReports } from './reports/canSeeMessageReports'; import { canActOnReport } from './reports/canActOnReport'; +import { userCan } from './userCan'; + export const authorizationService = { discussions: { canChangeSubscription: canChangeDiscussionSubscription @@ -11,5 +13,7 @@ export const authorizationService = { canActOnReport: canActOnReport, canSeeCommonReports: canSeeCommonReports, canSeeMessageReports: canSeeMessageReports - } + }, + + can: userCan }; \ No newline at end of file diff --git a/packages/core/src/services/authorization/userCan.ts b/packages/core/src/services/authorization/userCan.ts new file mode 100644 index 000000000..6c98b7e1a --- /dev/null +++ b/packages/core/src/services/authorization/userCan.ts @@ -0,0 +1,30 @@ +import * as z from 'zod'; + +import { logger } from '@logger'; +import { prisma } from '@toolkits'; +import { PermissionValidator } from '@validation'; + +export const userCan = async (userId: string, permission: z.infer): Promise => { + const user = await prisma.user.findUnique({ + where: { + id: userId + }, + select: { + permissions: true + } + }); + + if (!user) { + logger.debug('User cannot because it was not found', { + userId + }); + + return false; + } + + const can = user.permissions.includes(permission); + + logger.debug(`User ${can ? 'has' : 'does not have'} ${permission} permission`); + + return can; +}; \ No newline at end of file diff --git a/packages/core/src/services/index.ts b/packages/core/src/services/index.ts index 243265828..9ef0a7fbd 100644 --- a/packages/core/src/services/index.ts +++ b/packages/core/src/services/index.ts @@ -1,3 +1,4 @@ +export { roleService } from './roles'; export { userService } from './users'; export { voteService } from './votes'; export { cardService } from './cards'; diff --git a/packages/core/src/services/roles/commands/createRoleCommand.ts b/packages/core/src/services/roles/commands/createRoleCommand.ts new file mode 100644 index 000000000..47109ad5d --- /dev/null +++ b/packages/core/src/services/roles/commands/createRoleCommand.ts @@ -0,0 +1,42 @@ +import * as z from 'zod'; +import { Role, EventType } from '@prisma/client'; + +import { prisma } from '@toolkits'; +import { eventService } from '@services'; +import { PermissionValidator } from '@validation'; + +const schema = z.object({ + permissions: z.array(PermissionValidator) + .nonempty(), + + name: z.string() + .nonempty(), + + displayName: z.string() + .nonempty(), + + description: z.string() + .nonempty() +}); + +export const createRoleCommand = async (payload: z.infer): Promise => { + // Validate the payload + schema.parse(payload); + + // Create the role + const role = await prisma.role.create({ + data: payload + }); + + // Create event + eventService.create({ + type: EventType.RoleCreated, + payload: { + roleId: role.id, + permissions: role.permissions + } + }); + + // Return the created role + return role; +}; \ No newline at end of file diff --git a/packages/core/src/services/roles/commands/permissions/addRolePermissionCommand.ts b/packages/core/src/services/roles/commands/permissions/addRolePermissionCommand.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/src/services/roles/commands/permissions/removeRolePermissionCommand.ts b/packages/core/src/services/roles/commands/permissions/removeRolePermissionCommand.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/src/services/roles/commands/users/addUserToRoleCommand.ts b/packages/core/src/services/roles/commands/users/addUserToRoleCommand.ts new file mode 100644 index 000000000..518d0a172 --- /dev/null +++ b/packages/core/src/services/roles/commands/users/addUserToRoleCommand.ts @@ -0,0 +1,55 @@ +import { prisma } from '@toolkits'; +import { CommonError } from '@errors'; +import { logger as $logger } from '@logger'; +import { rebuildUserPermissionsCommand } from './rebuildUserPermissionsCommand'; + +export const addUserToRoleCommand = async (userId: string, roleId: string): Promise => { + const logger = $logger.child({ + userId, + roleId + }); + + // Find the user and check if they are already in that role + const userRoles = await prisma.user + .findUnique({ + where: { + id: userId + } + }) + .roles({ + where: { + id: roleId + } + }); + + if (userRoles.length) { + throw new CommonError('Cannot add the user twice to the same role', { + userId, + roleId + }); + } + + // Link the user to the role if they are not + logger.debug('Linking user to role'); + + await prisma.user.update({ + where: { + id: userId + }, + data: { + roles: { + connect: { + id: roleId + } + } + }, + select: { + id: true + } + }); + + logger.info('User linked to role'); + + // Rebuild the permissions + await rebuildUserPermissionsCommand(userId); +}; \ No newline at end of file diff --git a/packages/core/src/services/roles/commands/users/rebuildUserPermissionsCommand.ts b/packages/core/src/services/roles/commands/users/rebuildUserPermissionsCommand.ts new file mode 100644 index 000000000..b1fbe62a8 --- /dev/null +++ b/packages/core/src/services/roles/commands/users/rebuildUserPermissionsCommand.ts @@ -0,0 +1,56 @@ +import { User } from '@prisma/client'; + +import { logger } from '@logger'; +import { prisma } from '@toolkits'; +import { CommonError } from '@errors'; + +export const rebuildUserPermissionsCommand = async (userId: string): Promise => { + logger.debug(`Rebuilding roles for user (${userId})`, { userId }); + + // Find the user and all roles they have + const user = await prisma.user.findUnique({ + where: { + id: userId + }, + include: { + roles: { + select: { + permissions: true + } + } + } + }); + + // Check if the user exists + if (!user) { + throw new CommonError('Cannot rebuild permissions for non existent user'); + } + + // Create the new permission that the use has to have + const permissions = Array.from( + new Set(...user.roles.map(r => r.permissions)) + ); + + // Update the user permissions in the database + const updatedUser = await prisma.user.update({ + where: { + id: userId + }, + data: { + permissions + } + }); + + // Log the permission difference + const removedPermissions = user.permissions.filter(permission => !permission.includes(permission)); + const addedPermissions = permissions.filter(permission => !user.permissions.includes(permission)); + + logger.info('User permission update report', { + userId, + addedPermissions, + removedPermissions + }); + + // Return the updated user + return updatedUser; +}; \ No newline at end of file diff --git a/packages/core/src/services/roles/commands/users/removeUserFromRoleCommand.ts b/packages/core/src/services/roles/commands/users/removeUserFromRoleCommand.ts new file mode 100644 index 000000000..651dd8c3c --- /dev/null +++ b/packages/core/src/services/roles/commands/users/removeUserFromRoleCommand.ts @@ -0,0 +1,31 @@ +import { prisma } from '@toolkits'; +import { rebuildUserPermissionsCommand } from './rebuildUserPermissionsCommand'; +import { logger as $logger } from '@logger'; + +export const removeUserFromRoleCommand = async (userId: string, roleId: string): Promise => { + const logger = $logger.child({ + userId, + roleId + }); + + // Unlink the user from the role + logger.debug('Removing user from role'); + + await prisma.user.update({ + where: { + id: userId + }, + data: { + roles: { + disconnect: { + id: roleId + } + } + } + }); + + logger.info('User removed from role'); + + // Rebuild the user permissions + await rebuildUserPermissionsCommand(userId); +}; \ No newline at end of file diff --git a/packages/core/src/services/roles/index.ts b/packages/core/src/services/roles/index.ts new file mode 100644 index 000000000..14648622c --- /dev/null +++ b/packages/core/src/services/roles/index.ts @@ -0,0 +1,30 @@ +import { createRoleCommand } from './commands/createRoleCommand'; + +import { addUserToRoleCommand } from './commands/users/addUserToRoleCommand'; +import { removeUserFromRoleCommand } from './commands/users/removeUserFromRoleCommand'; +import { rebuildUserPermissionsCommand } from './commands/users/rebuildUserPermissionsCommand'; + +export const roleService = { + /** + * Create new role + */ + create: createRoleCommand, + + users: { + /** + * Add one user to one role + */ + addToRole: addUserToRoleCommand, + + /** + * Remove one role from one user + */ + removeFromRole: removeUserFromRoleCommand, + + /** + * Rebuild the user permissions + */ + rebuildPermissions: rebuildUserPermissionsCommand + }, + permissions: {} +}; \ No newline at end of file From 1ae24b29a51b8a3e2bdca70a4d08851bf994b926 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 22 Apr 2021 10:13:01 +0300 Subject: [PATCH 02/29] GraphQL role initial additions Signed-off-by: Alexander Ivanov --- .run/GraphQL.run.xml | 13 +++-- .../migration.sql | 36 ++++++++++++ .../20210422054519_role_events/migration.sql | 14 +++++ packages/core/prisma/seed.ts | 56 ++++++++++--------- packages/core/src/domain/validation/index.ts | 14 ++++- .../graphql/src/generated/nexus-typegen.ts | 46 ++++++++++++++- packages/graphql/src/generated/schema.graphql | 30 ++++++++++ .../schema/Shared/Inputs/Paginate.input.ts | 12 ++++ .../Types/Roles/Queries/GetRoles.query.ts | 21 +++++++ .../src/schema/Types/Roles/Types/Role.type.ts | 16 ++++++ .../graphql/src/schema/Types/Roles/index.ts | 9 +++ .../src/schema/Types/Users/Types/User.type.ts | 17 ++++++ packages/graphql/src/schema/index.ts | 7 ++- 13 files changed, 254 insertions(+), 37 deletions(-) create mode 100644 packages/core/prisma/migrations/20210422051828_roles_and_permissions_migration/migration.sql create mode 100644 packages/core/prisma/migrations/20210422054519_role_events/migration.sql create mode 100644 packages/graphql/src/schema/Shared/Inputs/Paginate.input.ts create mode 100644 packages/graphql/src/schema/Types/Roles/Queries/GetRoles.query.ts create mode 100644 packages/graphql/src/schema/Types/Roles/Types/Role.type.ts create mode 100644 packages/graphql/src/schema/Types/Roles/index.ts diff --git a/.run/GraphQL.run.xml b/.run/GraphQL.run.xml index 9b88fdc09..125e89a31 100644 --- a/.run/GraphQL.run.xml +++ b/.run/GraphQL.run.xml @@ -1,12 +1,13 @@ - - + + -