From 6f44f3ae2103c0210db085126949ec8102cab29f Mon Sep 17 00:00:00 2001 From: = Date: Sat, 19 Oct 2024 20:10:38 +0530 Subject: [PATCH] feat: added backend endpoint for new identitiy additional privilege permission system --- backend/src/@types/fastify.d.ts | 2 + ...ity-project-additional-privilege-router.ts | 310 +++++++++++++++++ backend/src/ee/routes/v2/index.ts | 5 + ...ity-project-additional-privilege-v2-dal.ts | 12 + ...project-additional-privilege-v2-service.ts | 323 ++++++++++++++++++ ...y-project-additional-privilege-v2-types.ts | 53 +++ ...ty-project-additional-privilege-service.ts | 2 +- backend/src/lib/api-docs/constants.ts | 40 +++ backend/src/server/routes/index.ts | 10 + .../identitiy-additional-privilege.ts | 7 + .../routes/santizedSchemas/permission.ts | 5 + 11 files changed, 768 insertions(+), 1 deletion(-) create mode 100644 backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts create mode 100644 backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts create mode 100644 backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts create mode 100644 backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts create mode 100644 backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 21c44a3b5..7eb0c6cb7 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -13,6 +13,7 @@ import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secr import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; +import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; @@ -177,6 +178,7 @@ declare module "fastify" { dynamicSecretLease: TDynamicSecretLeaseServiceFactory; projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory; identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; + identityProjectAdditionalPrivilegeV2: TIdentityProjectAdditionalPrivilegeV2ServiceFactory; secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts new file mode 100644 index 000000000..884738fca --- /dev/null +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -0,0 +1,310 @@ +import { packRules } from "@casl/ability/extra"; +import slugify from "@sindresorhus/slugify"; +import ms from "ms"; +import { z } from "zod"; + +import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; +import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; +import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create an additional privilege for identity.", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), + projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), + slug: z + .string() + .min(1) + .max(60) + .trim() + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .optional() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), + permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), + type: z.discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(false) + }), + z.object({ + isTemporary: z.literal(true), + temporaryMode: z + .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryAccessStartTime) + }) + ]) + }), + response: { + 200: z.object({ + privilege: SanitizedIdentityPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilegeV2.create({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + projectId: req.body.projectId, + identityId: req.body.identityId, + ...req.body.type, + slug: req.body.slug || slugify(alphaNumericNanoId(8).toLowerCase()), + permissions: JSON.stringify(packRules(req.body.permissions)) + }); + return { privilege }; + } + }); + + server.route({ + method: "PATCH", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update a specific privilege of an identity.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id) + }), + body: z.object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), + permissions: ProjectPermissionV2Schema.array() + .optional() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), + type: z.discriminatedUnion("isTemporary", [ + z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }), + z.object({ + isTemporary: z.literal(true).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary), + temporaryMode: z + .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => typeof val === "undefined" || ms(val) > 0, "Temporary range must be a positive number") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryAccessStartTime) + }) + ]) + }), + response: { + 200: z.object({ + privilege: SanitizedIdentityPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilegeV2.updateById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + id: req.params.id, + data: { + ...req.body, + ...req.body.type, + permissions: req.body.permissions + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + JSON.stringify(packRules(req.body.permissions)) + : undefined + } + }); + return { privilege }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete a specific privilege of an identity.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.DELETE.id) + }), + response: { + 200: z.object({ + privilege: SanitizedIdentityPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilegeV2.deleteById({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { privilege }; + } + }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + description: "Retrieve details of a specific privilege by privilege id.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_ID.id) + }), + response: { + 200: z.object({ + privilege: SanitizedIdentityPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsById({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { privilege }; + } + }); + + server.route({ + method: "GET", + url: "/slug/:privilegeSlug", + config: { + rateLimit: readLimit + }, + schema: { + description: "Retrieve details of a specific privilege by privilege slug.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_SLUG.slug) + }), + querystring: z.object({ + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_SLUG.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_SLUG.projectSlug) + }), + response: { + 200: z.object({ + privilege: SanitizedIdentityPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsBySlug({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + slug: req.params.privilegeSlug, + ...req.query + }); + return { privilege }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + description: "List of a specific privilege of an identity in a project.", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.LIST.identityId), + projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.LIST.projectId) + }), + response: { + 200: z.object({ + privileges: SanitizedIdentityPrivilegeSchema.omit({ permissions: true }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privileges = await server.services.identityProjectAdditionalPrivilegeV2.listIdentityProjectPrivileges({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + return { + privileges + }; + } + }); +}; diff --git a/backend/src/ee/routes/v2/index.ts b/backend/src/ee/routes/v2/index.ts index 980e4777b..bede5a1cf 100644 --- a/backend/src/ee/routes/v2/index.ts +++ b/backend/src/ee/routes/v2/index.ts @@ -1,3 +1,4 @@ +import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerProjectRoleRouter } from "./project-role-router"; export const registerV2EERoutes = async (server: FastifyZodProvider) => { @@ -8,4 +9,8 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => { }, { prefix: "/workspace" } ); + + await server.register(registerIdentityProjectAdditionalPrivilegeRouter, { + prefix: "/identity-project-additional-privilege" + }); }; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts new file mode 100644 index 000000000..a7d8794a4 --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts @@ -0,0 +1,12 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityProjectAdditionalPrivilegeV2DALFactory = ReturnType< + typeof identityProjectAdditionalPrivilegeV2DALFactory +>; + +export const identityProjectAdditionalPrivilegeV2DALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); + return orm; +}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts new file mode 100644 index 000000000..4c842d864 --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -0,0 +1,323 @@ +import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; + +import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; +import { + IdentityProjectAdditionalPrivilegeTemporaryMode, + TCreateIdentityPrivilegeDTO, + TDeleteIdentityPrivilegeByIdDTO, + TGetIdentityPrivilegeDetailsByIdDTO, + TGetIdentityPrivilegeDetailsBySlugDTO, + TListIdentityPrivilegesDTO, + TUpdateIdentityPrivilegeByIdDTO +} from "./identity-project-additional-privilege-v2-types"; + +type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = { + identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; + identityProjectDAL: Pick; + projectDAL: Pick; + permissionService: Pick; +}; + +export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< + typeof identityProjectAdditionalPrivilegeV2ServiceFactory +>; + +export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ + identityProjectAdditionalPrivilegeDAL, + identityProjectDAL, + projectDAL, + permissionService +}: TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep) => { + const create = async ({ + slug, + actor, + actorId, + projectId, + actorOrgId, + identityId, + permissions: customPermission, + actorAuthMethod, + ...dto + }: TCreateIdentityPrivilegeDTO) => { + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + + const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + + if (!dto.isTemporary) { + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ + projectMembershipId: identityProjectMembership.id, + slug, + permissions: customPermission + }); + + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; + } + + const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ + projectMembershipId: identityProjectMembership.id, + slug, + permissions: customPermission, + isTemporary: true, + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: dto.temporaryRange, + temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) + }); + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; + }; + + const updateById = async ({ + id, + data, + actorOrgId, + actor, + actorId, + actorAuthMethod + }: TUpdateIdentityPrivilegeByIdDTO) => { + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); + if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" }); + + const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); + if (!identityProjectMembership) + throw new NotFoundError({ + message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` + }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + + if (data?.slug) { + const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug: data.slug, + projectMembershipId: identityProjectMembership.id + }); + if (existingSlug && existingSlug.id !== identityPrivilege.id) + throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + } + + const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; + if (isTemporary) { + const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; + const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { + ...data, + temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), + temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) + }); + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; + } + + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { + ...data, + isTemporary: false, + temporaryAccessStartTime: null, + temporaryAccessEndTime: null, + temporaryRange: null, + temporaryMode: null + }); + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; + }; + + const deleteById = async ({ actorId, id, actor, actorOrgId, actorAuthMethod }: TDeleteIdentityPrivilegeByIdDTO) => { + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); + if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" }); + + const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); + if (!identityProjectMembership) + throw new NotFoundError({ + message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` + }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + + const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); + return { + ...deletedPrivilege, + permissions: unpackPermissions(deletedPrivilege.permissions) + }; + }; + + const getPrivilegeDetailsById = async ({ + id, + actorOrgId, + actor, + actorId, + actorAuthMethod + }: TGetIdentityPrivilegeDetailsByIdDTO) => { + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); + if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" }); + + const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); + if (!identityProjectMembership) + throw new NotFoundError({ + message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` + }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + + return { + ...identityPrivilege, + permissions: unpackPermissions(identityPrivilege.permissions) + }; + }; + + const getPrivilegeDetailsBySlug = async ({ + identityId, + slug, + projectSlug, + actorOrgId, + actor, + actorId, + actorAuthMethod + }: TGetIdentityPrivilegeDetailsBySlugDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new NotFoundError({ message: "Project not found" }); + const projectId = project.id; + + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" }); + + return { + ...identityPrivilege, + permissions: unpackPermissions(identityPrivilege.permissions) + }; + }; + + const listIdentityProjectPrivileges = async ({ + identityId, + actorOrgId, + actor, + actorId, + actorAuthMethod, + projectId + }: TListIdentityPrivilegesDTO) => { + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + + const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ + projectMembershipId: identityProjectMembership.id + }); + return identityPrivileges; + }; + + return { + getPrivilegeDetailsById, + getPrivilegeDetailsBySlug, + listIdentityProjectPrivileges, + create, + updateById, + deleteById + }; +}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts new file mode 100644 index 000000000..9825b0ef3 --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts @@ -0,0 +1,53 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum IdentityProjectAdditionalPrivilegeTemporaryMode { + Relative = "relative" +} + +export type TCreateIdentityPrivilegeDTO = { + permissions: unknown; + identityId: string; + projectId: string; + slug: string; +} & ( + | { + isTemporary: false; + } + | { + isTemporary: true; + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } +) & + Omit; + +export type TUpdateIdentityPrivilegeByIdDTO = { id: string } & Omit & { + data: Partial<{ + permissions: unknown; + slug: string; + isTemporary: boolean; + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + }>; + }; + +export type TDeleteIdentityPrivilegeByIdDTO = Omit & { + id: string; +}; + +export type TGetIdentityPrivilegeDetailsByIdDTO = Omit & { + id: string; +}; + +export type TListIdentityPrivilegesDTO = Omit & { + identityId: string; + projectId: string; +}; + +export type TGetIdentityPrivilegeDetailsBySlugDTO = Omit & { + slug: string; + identityId: string; + projectSlug: string; +}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 185c78b33..cd6565660 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -278,7 +278,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ecc8870d3..af51d323d 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -971,6 +971,46 @@ export const PROJECT_USER_ADDITIONAL_PRIVILEGE = { } }; +export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = { + CREATE: { + identityId: "The ID of the identity to create.", + projectId: "The ID of the project of the identity in.", + slug: "The slug of the privilege to create.", + permission: "The permission for the privilege.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + UPDATE: { + id: "The id of the privilege of the identity.", + identityId: "The ID of the identity to update.", + slug: "The slug of the privilege to update.", + privilegePermission: "The permission for the privilege.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + DELETE: { + id: "the id of the privilege of the identity.", + identityId: "The ID of the identity to delete.", + slug: "The slug of the privilege to delete." + }, + GET_BY_SLUG: { + projectSlug: "The slug of the project of the identity in.", + identityId: "The ID of the identity to list.", + slug: "The slug of the privilege." + }, + GET_BY_ID: { + id: "The id of the privilege of the identity." + }, + LIST: { + projectId: "The ID of the project of the identity in.", + identityId: "The ID of the identity to list." + } +}; + export const INTEGRATION_AUTH = { GET: { integrationAuthId: "The id of integration authentication object." diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 827008ca4..42b581c6e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -33,6 +33,7 @@ import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; +import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal"; @@ -1076,6 +1077,14 @@ export const registerRoutes = async ( permissionService, identityProjectDAL }); + + const identityProjectAdditionalPrivilegeV2Service = identityProjectAdditionalPrivilegeV2ServiceFactory({ + projectDAL, + identityProjectAdditionalPrivilegeDAL, + permissionService, + identityProjectDAL + }); + const identityTokenAuthService = identityTokenAuthServiceFactory({ identityTokenAuthDAL, identityDAL, @@ -1325,6 +1334,7 @@ export const registerRoutes = async ( telemetry: telemetryService, projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, + identityProjectAdditionalPrivilegeV2: identityProjectAdditionalPrivilegeV2Service, secretSharing: secretSharingService, userEngagement: userEngagementService, externalKms: externalKmsService, diff --git a/backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts b/backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts new file mode 100644 index 000000000..e44b9af4e --- /dev/null +++ b/backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts @@ -0,0 +1,7 @@ +import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; + +import { UnpackedPermissionSchema } from "./permission"; + +export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({ + permissions: UnpackedPermissionSchema.array() +}); diff --git a/backend/src/server/routes/santizedSchemas/permission.ts b/backend/src/server/routes/santizedSchemas/permission.ts index 94c1dc57c..5b5fdccac 100644 --- a/backend/src/server/routes/santizedSchemas/permission.ts +++ b/backend/src/server/routes/santizedSchemas/permission.ts @@ -1,3 +1,5 @@ +import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, unpackRules } from "@casl/ability/extra"; import { z } from "zod"; export const UnpackedPermissionSchema = z.object({ @@ -9,3 +11,6 @@ export const UnpackedPermissionSchema = z.object({ conditions: z.unknown().optional(), inverted: z.boolean().optional() }); + +export const unpackPermissions = (permissions: unknown) => + UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[]));