diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 54e220abf..0a9aff8f0 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -5,6 +5,7 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; +import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -123,6 +124,7 @@ declare module "fastify" { dynamicSecret: TDynamicSecretServiceFactory; dynamicSecretLease: TDynamicSecretLeaseServiceFactory; projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory; + identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 9b4bfec96..7c14eb781 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -38,6 +38,9 @@ import { TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate, + TIdentityProjectAdditionalPrivilege, + TIdentityProjectAdditionalPrivilegeInsert, + TIdentityProjectAdditionalPrivilegeUpdate, TIdentityProjectMembershipRole, TIdentityProjectMembershipRoleInsert, TIdentityProjectMembershipRoleUpdate, @@ -302,6 +305,11 @@ declare module "knex/types/tables" { TIdentityProjectMembershipRoleInsert, TIdentityProjectMembershipRoleUpdate >; + [TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType< + TIdentityProjectAdditionalPrivilege, + TIdentityProjectAdditionalPrivilegeInsert, + TIdentityProjectAdditionalPrivilegeUpdate + >; [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, diff --git a/backend/src/db/migrations/20240315102813_machine-identity-additional-privilege.ts b/backend/src/db/migrations/20240315102813_machine-identity-additional-privilege.ts new file mode 100644 index 000000000..143fa6bdd --- /dev/null +++ b/backend/src/db/migrations/20240315102813_machine-identity-additional-privilege.ts @@ -0,0 +1,34 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityProjectAdditionalPrivilege))) { + await knex.schema.createTable(TableName.IdentityProjectAdditionalPrivilege, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.string("slug", 60).notNullable(); + t.string("description"); + t.uuid("projectMembershipId").notNullable(); + t.foreign("projectMembershipId") + .references("id") + .inTable(TableName.IdentityProjectMembership) + .onDelete("CASCADE"); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.jsonb("permissions").notNullable(); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege); +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege); + await knex.schema.dropTableIfExists(TableName.IdentityProjectAdditionalPrivilege); +} diff --git a/backend/src/db/schemas/identity-project-additional-privilege.ts b/backend/src/db/schemas/identity-project-additional-privilege.ts new file mode 100644 index 000000000..0e548dd93 --- /dev/null +++ b/backend/src/db/schemas/identity-project-additional-privilege.ts @@ -0,0 +1,33 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityProjectAdditionalPrivilegeSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + slug: z.string(), + description: z.string().nullable().optional(), + projectMembershipId: z.string().uuid(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + permissions: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityProjectAdditionalPrivilege = z.infer; +export type TIdentityProjectAdditionalPrivilegeInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TIdentityProjectAdditionalPrivilegeUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 96993dc10..361ae2e0d 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -10,6 +10,7 @@ export * from "./git-app-org"; export * from "./identities"; export * from "./identity-access-tokens"; export * from "./identity-org-memberships"; +export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; export * from "./identity-project-memberships"; export * from "./identity-ua-client-secrets"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 8af8951fd..3cb11d694 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -44,6 +44,7 @@ export enum TableName { IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", + IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", ScimToken = "scim_tokens", SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts new file mode 100644 index 000000000..622b003dd --- /dev/null +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -0,0 +1,171 @@ +import ms from "ms"; +import { z } from "zod"; + +import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; +import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z.union([ + z.object({ + identityId: z.string(), + projectId: z.string(), + slug: z.string().max(60).trim(), + name: z.string().trim(), + description: z.string().trim().optional(), + permissions: z.any().array(), + isTemporary: z.literal(false).default(false) + }), + z.object({ + identityId: z.string(), + projectId: z.string(), + slug: z.string().max(60).trim(), + name: z.string().trim(), + description: z.string().trim().optional(), + permissions: z.any().array(), + isTemporary: z.literal(true), + temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + ]), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.create({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + ...req.body, + permissions: JSON.stringify(req.body.permissions) + }); + return { privilege }; + } + }); + + server.route({ + url: "/:privilegeId", + method: "PATCH", + schema: { + params: z.object({ + privilegeId: z.string() + }), + body: z + .object({ + slug: z.string().max(60).trim(), + name: z.string().trim(), + description: z.string().trim().optional(), + permissions: z.any().array(), + isTemporary: z.boolean(), + temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + .partial(), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.updateById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + ...req.body, + permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined, + privilegeId: req.params.privilegeId + }); + return { privilege }; + } + }); + + server.route({ + url: "/:privilegeId", + method: "DELETE", + schema: { + params: z.object({ + privilegeId: z.string() + }), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.deleteById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + privilegeId: req.params.privilegeId + }); + return { privilege }; + } + }); + + server.route({ + url: "/:privilegeId", + method: "GET", + schema: { + params: z.object({ + privilegeId: z.string() + }), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + privilegeId: req.params.privilegeId + }); + return { privilege }; + } + }); + + server.route({ + url: "/permissions", + method: "POST", + schema: { + body: z.object({ + identityId: z.string(), + projectId: z.string() + }), + response: { + 200: z.object({ + privileges: IdentityProjectAdditionalPrivilegeSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privileges = await server.services.identityProjectAdditionalPrivilege.listIdentityProjectPrivileges({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.body.projectId, + identityId: req.body.identityId + }); + return { privileges }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 0d7b45ded..72021c81f 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,5 +1,6 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; +import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; import { registerOrgRoleRouter } from "./org-role-router"; @@ -55,6 +56,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register( async (privilegeRouter) => { await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" }); + await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" }); }, { prefix: "/additional-privilege" } ); diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts new file mode 100644 index 000000000..26252f2d1 --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-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 TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType< + typeof identityProjectAdditionalPrivilegeDALFactory +>; + +export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); + return orm; +}; 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 new file mode 100644 index 000000000..59729917c --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -0,0 +1,259 @@ +import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; + +import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; +import { + IdentityProjectAdditionalPrivilegeTemporaryMode, + TCreateIdentityPrivilegeDTO, + TDeleteIdentityPrivilegeDTO, + TGetIdentityPrivilegeDetailsDTO, + TListIdentityPrivilegesDTO, + TUpdateIdentityPrivilegeDTO +} from "./identity-project-additional-privilege-types"; + +type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { + identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; + identityProjectDAL: Pick; + permissionService: Pick; +}; + +export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< + typeof identityProjectAdditionalPrivilegeServiceFactory +>; + +export const identityProjectAdditionalPrivilegeServiceFactory = ({ + identityProjectAdditionalPrivilegeDAL, + identityProjectDAL, + permissionService +}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => { + const create = async ({ + name, + slug, + actor, + actorId, + projectId, + identityId, + permissions: customPermission, + actorOrgId, + description, + ...dto + }: TCreateIdentityPrivilegeDTO) => { + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityId, + identityProjectMembership.projectId, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to delete 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, + name, + description + }); + return additionalPrivilege; + } + + const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ + projectMembershipId: identityProjectMembership.id, + slug, + permissions: customPermission, + name, + description, + isTemporary: true, + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: dto.temporaryRange, + temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) + }); + return additionalPrivilege; + }; + + const updateById = async ({ privilegeId, actorOrgId, actor, actorId, ...dto }: TUpdateIdentityPrivilegeDTO) => { + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(privilegeId); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + + const identityProjectMembership = await identityProjectDAL.findById(identityPrivilege.projectMembershipId); + if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + + if (dto?.slug) { + const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug: dto.slug, + projectMembershipId: identityProjectMembership.id + }); + if (existingSlug && existingSlug.id !== identityPrivilege.id) + throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + } + + const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : identityPrivilege.isTemporary; + if (isTemporary) { + const temporaryAccessStartTime = dto?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; + const temporaryRange = dto?.temporaryRange || identityPrivilege?.temporaryRange; + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { + ...dto, + temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), + temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) + }); + return additionalPrivilege; + } + + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { + ...dto, + isTemporary: false, + temporaryAccessStartTime: null, + temporaryAccessEndTime: null, + temporaryRange: null, + temporaryMode: null + }); + return additionalPrivilege; + }; + + const deleteById = async ({ actorId, actor, actorOrgId, privilegeId }: TDeleteIdentityPrivilegeDTO) => { + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(privilegeId); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + + const identityProjectMembership = await identityProjectDAL.findById(identityPrivilege.projectMembershipId); + if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + + const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); + return deletedPrivilege; + }; + + const getPrivilegeDetailsById = async ({ + privilegeId, + actorOrgId, + actor, + actorId + }: TGetIdentityPrivilegeDetailsDTO) => { + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(privilegeId); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + + const identityProjectMembership = await identityProjectDAL.findById(identityPrivilege.projectMembershipId); + if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + + return identityPrivilege; + }; + + const listIdentityProjectPrivileges = async ({ + projectId, + identityId, + actorOrgId, + actor, + actorId + }: TListIdentityPrivilegesDTO) => { + const identityProjectMembership = await identityProjectDAL.findOne({ projectId, identityId }); + if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + + const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivileges) throw new BadRequestError({ message: "Identity additional privilege not found" }); + return identityPrivileges; + }; + + return { + create, + updateById, + deleteById, + getPrivilegeDetailsById, + listIdentityProjectPrivileges + }; +}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts new file mode 100644 index 000000000..76a8ffb62 --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts @@ -0,0 +1,51 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum IdentityProjectAdditionalPrivilegeTemporaryMode { + Relative = "relative" +} + +export type TCreateIdentityPrivilegeDTO = ( + | { + permissions: unknown; + identityId: string; + projectId: string; + name: string; + slug: string; + description?: string; + isTemporary: false; + } + | { + permissions: unknown; + identityId: string; + projectId: string; + name: string; + slug: string; + description?: string; + isTemporary: true; + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } +) & + Omit; + +export type TUpdateIdentityPrivilegeDTO = { privilegeId: string } & Omit & + Partial<{ + permissions: unknown; + name: string; + slug: string; + description?: string; + isTemporary: boolean; + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + }>; + +export type TDeleteIdentityPrivilegeDTO = Omit & { privilegeId: string }; + +export type TGetIdentityPrivilegeDetailsDTO = Omit & { privilegeId: string }; + +export type TListIdentityPrivilegesDTO = Omit & { + identityId: string; + projectId: string; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4599a732b..c33f62029 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -11,6 +11,8 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal"; import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue"; import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; +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 { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { licenseDALFactory } from "@app/ee/services/license/license-dal"; @@ -177,6 +179,7 @@ export const registerRoutes = async ( const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db); const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); + const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db); const identityUaDAL = identityUaDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); @@ -556,6 +559,11 @@ export const registerRoutes = async ( identityProjectMembershipRoleDAL, projectRoleDAL }); + const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({ + identityProjectAdditionalPrivilegeDAL, + permissionService, + identityProjectDAL + }); const identityUaService = identityUaServiceFactory({ identityOrgMembershipDAL, permissionService, @@ -647,7 +655,8 @@ export const registerRoutes = async ( scim: scimService, secretBlindIndex: secretBlindIndexService, telemetry: telemetryService, - projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService + projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, + identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService }); server.decorate("store", { diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index d170f87e6..81a59e995 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -156,6 +156,20 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) identityId: z.string(), createdAt: z.date(), updatedAt: z.date(), + additionalPrivileges: z.array( + z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + description: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + createdAt: z.date() + }) + ), roles: z.array( z.object({ id: z.string(), diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index dd3ba04f4..b6de21d05 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -25,6 +25,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .leftJoin( + TableName.IdentityProjectAdditionalPrivilege, + `${TableName.IdentityProjectMembership}.id`, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` + ) .select( db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), @@ -42,7 +47,29 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole) + db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), + db.ref("name").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApName"), + db.ref("description").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApDescription"), + db.ref("slug").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApSlug"), + db + .ref("temporaryMode") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), + db.ref("createdAt").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApCreatedAt"), + db + .ref("temporaryRange") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessEndTime") ); const members = sqlNestRelationships({ @@ -86,6 +113,33 @@ export const identityProjectDALFactory = (db: TDbClient) => { temporaryAccessStartTime, isTemporary }) + }, + { + label: "additionalPrivileges" as const, + key: "identityApId", + mapper: ({ + identityApId, + identityApDescription, + identityApName, + identityApSlug, + identityApIsTemporary, + identityApTemporaryMode, + identityApTemporaryRange, + identityApCreatedAt, + identityApTemporaryAccessEndTime, + identityApTemporaryAccessStartTime + }) => ({ + id: identityApId, + name: identityApName, + description: identityApDescription, + slug: identityApSlug, + temporaryRange: identityApTemporaryRange, + temporaryMode: identityApTemporaryMode, + temporaryAccessEndTime: identityApTemporaryAccessEndTime, + temporaryAccessStartTime: identityApTemporaryAccessStartTime, + isTemporary: identityApIsTemporary, + createdAt: identityApCreatedAt + }) } ] });