diff --git a/apps/api/src/common/collective-authorities.ts b/apps/api/src/common/collective-authorities.ts index 3504e3219..a9220415e 100644 --- a/apps/api/src/common/collective-authorities.ts +++ b/apps/api/src/common/collective-authorities.ts @@ -121,16 +121,6 @@ export const getCollectiveEnvironmentAuthorities = async ( }, role: { OR: [ - { - projects: { - some: { - projectId: environment.project.id, - environments: { - none: {} - } - } - } - }, { projects: { some: { @@ -142,6 +132,12 @@ export const getCollectiveEnvironmentAuthorities = async ( } } } + }, + // Check if the user has the WORKSPACE_ADMIN authority + { + authorities: { + has: Authority.WORKSPACE_ADMIN + } } ] } diff --git a/apps/api/src/event/event.e2e.spec.ts b/apps/api/src/event/event.e2e.spec.ts index fb46022a5..4789b7c59 100644 --- a/apps/api/src/event/event.e2e.spec.ts +++ b/apps/api/src/event/event.e2e.spec.ts @@ -438,9 +438,7 @@ describe('Event Controller Tests', () => { description: 'Some description', colorCode: '#000000', authorities: [], - projectEnvironments: [ - { projectSlug: project.slug, environmentSlugs: [] } - ] + projectEnvironments: [{ projectSlug: project.slug }] } ) diff --git a/apps/api/src/workspace-role/dto/create-workspace-role/create-workspace-role.ts b/apps/api/src/workspace-role/dto/create-workspace-role/create-workspace-role.ts index 772991d12..423a2cb72 100644 --- a/apps/api/src/workspace-role/dto/create-workspace-role/create-workspace-role.ts +++ b/apps/api/src/workspace-role/dto/create-workspace-role/create-workspace-role.ts @@ -6,6 +6,18 @@ import { IsString, ValidateNested } from 'class-validator' +import { Type } from 'class-transformer' + +class ProjectEnvironments { + @IsString() + @IsNotEmpty() + readonly projectSlug: string + + @IsArray() + @IsOptional() + @IsNotEmpty({ each: true }) + readonly environmentSlugs?: string[] +} export class CreateWorkspaceRole { @IsString() @@ -26,15 +38,6 @@ export class CreateWorkspaceRole { @IsArray() @IsOptional() @ValidateNested({ each: true }) + @Type(() => ProjectEnvironments) readonly projectEnvironments?: ProjectEnvironments[] } - -class ProjectEnvironments { - @IsString() - @IsNotEmpty() - readonly projectSlug: string - - @IsArray() - @IsNotEmpty({ each: true }) - readonly environmentSlugs: string[] -} diff --git a/apps/api/src/workspace-role/service/workspace-role.service.ts b/apps/api/src/workspace-role/service/workspace-role.service.ts index 9b4ef93c1..ccbb34196 100644 --- a/apps/api/src/workspace-role/service/workspace-role.service.ts +++ b/apps/api/src/workspace-role/service/workspace-role.service.ts @@ -107,46 +107,73 @@ export class WorkspaceRoleService { if (dto.projectEnvironments) { // Create the project associations const projectSlugToIdMap = await this.getProjectSlugToIdMap( - dto.projectEnvironments - .map((pe) => pe.projectSlug) - .filter((slug) => slug) + dto.projectEnvironments.map((pe) => pe.projectSlug) ) for (const pe of dto.projectEnvironments) { const projectId = projectSlugToIdMap.get(pe.projectSlug) if (projectId) { - //Check if all environments are part of the project - const project = await this.prisma.project.findFirst({ - where: { - id: projectId, - AND: pe.environmentSlugs.map((slug) => ({ - environments: { - some: { - slug: slug + if (pe.environmentSlugs && pe.environmentSlugs.length === 0) + throw new BadRequestException( + `EnvironmentSlugs in the project ${pe.projectSlug} are required` + ) + if (pe.environmentSlugs) { + //Check if all environments are part of the project + const project = await this.prisma.project.findFirst({ + where: { + id: projectId, + AND: pe.environmentSlugs.map((slug) => ({ + environments: { + some: { + slug: slug + } } - } - })) + })) + } + }) + + if (!project) { + throw new BadRequestException( + `All environmentSlugs in the project ${pe.projectSlug} are not part of the project` + ) } - }) - if (!project) { - throw new BadRequestException( - `All environmentSlugs in the project ${pe.projectSlug} are not part of the project` - ) + // Check if the user has read authority over all the environments + for (const environmentSlug of pe.environmentSlugs) { + try { + await this.authorityCheckerService.checkAuthorityOverEnvironment( + { + userId: user.id, + entity: { + slug: environmentSlug + }, + authorities: [Authority.READ_ENVIRONMENT], + prisma: this.prisma + } + ) + } catch { + throw new UnauthorizedException( + `User does not have read authority over environment ${environmentSlug}` + ) + } + } } - // Create the project workspace role association with the environments accessible on the project op.push( this.prisma.projectWorkspaceRoleAssociation.create({ data: { roleId: workspaceRoleId, projectId: projectId, - environments: { + environments: pe.environmentSlugs && { connect: pe.environmentSlugs.map((slug) => ({ slug })) } } }) ) + } else { + throw new NotFoundException( + `Project with slug ${pe.projectSlug} not found` + ) } } } @@ -264,34 +291,57 @@ export class WorkspaceRoleService { }) const projectSlugToIdMap = await this.getProjectSlugToIdMap( - dto.projectEnvironments - .map((pe) => pe.projectSlug) - .filter((slug) => slug) + dto.projectEnvironments.map((pe) => pe.projectSlug) ) for (const pe of dto.projectEnvironments) { const projectId = projectSlugToIdMap.get(pe.projectSlug) if (projectId) { - //Check if all environments are part of the project - const project = await this.prisma.project.findFirst({ - where: { - id: projectId, - AND: pe.environmentSlugs.map((slug) => ({ - environments: { - some: { - slug: slug + if (pe.environmentSlugs && pe.environmentSlugs.length === 0) + throw new BadRequestException( + `EnvironmentSlugs in the project ${pe.projectSlug} are required` + ) + if (pe.environmentSlugs) { + //Check if all environments are part of the project + const project = await this.prisma.project.findFirst({ + where: { + id: projectId, + AND: pe.environmentSlugs.map((slug) => ({ + environments: { + some: { + slug: slug + } } - } - })) + })) + } + }) + + if (!project) { + throw new BadRequestException( + `All environmentSlugs in the project ${pe.projectSlug} are not part of the project` + ) } - }) - if (!project) { - throw new BadRequestException( - `All environmentSlugs in the project ${pe.projectSlug} are not part of the project` - ) + // Check if the user has read authority over all the environments + for (const environmentSlug of pe.environmentSlugs) { + try { + await this.authorityCheckerService.checkAuthorityOverEnvironment( + { + userId: user.id, + entity: { + slug: environmentSlug + }, + authorities: [Authority.READ_ENVIRONMENT], + prisma: this.prisma + } + ) + } catch { + throw new UnauthorizedException( + `User does not have update authority over environment ${environmentSlug}` + ) + } + } } - // Create or Update the project workspace role association with the environments accessible on the project await this.prisma.projectWorkspaceRoleAssociation.upsert({ where: { @@ -301,7 +351,7 @@ export class WorkspaceRoleService { } }, update: { - environments: { + environments: pe.environmentSlugs && { set: [], connect: pe.environmentSlugs.map((slug) => ({ slug })) } @@ -309,11 +359,15 @@ export class WorkspaceRoleService { create: { roleId: workspaceRoleId, projectId: projectId, - environments: { + environments: pe.environmentSlugs && { connect: pe.environmentSlugs.map((slug) => ({ slug })) } } }) + } else { + throw new NotFoundException( + `Project with slug ${pe.projectSlug} not found` + ) } } } diff --git a/apps/api/src/workspace-role/workspace-role.e2e.spec.ts b/apps/api/src/workspace-role/workspace-role.e2e.spec.ts index 3bb4790e4..0981c564a 100644 --- a/apps/api/src/workspace-role/workspace-role.e2e.spec.ts +++ b/apps/api/src/workspace-role/workspace-role.e2e.spec.ts @@ -445,6 +445,36 @@ describe('Workspace Role Controller Tests', () => { }) expect(response.statusCode).toBe(400) }) + + it("should not be able to add environments that the user doesn't have read access to", async () => { + await prisma.environment.create({ + data: { + name: 'development', + slug: 'development', + projectId: projects[0].id + } + }) + const response = await app.inject({ + method: 'POST', + url: `/workspace-role/${workspaceAlice.slug}`, + payload: { + name: 'Test Role 2', + description: 'Test Role Description', + colorCode: '#0000FF', + authorities: [Authority.READ_ENVIRONMENT, Authority.READ_VARIABLE], + projectEnvironments: [ + { + projectSlug: projects[0].slug, + environmentSlugs: ['development'] + } + ] + }, + headers: { + 'x-e2e-user-email': charlie.email + } + }) + expect(response.statusCode).toBe(401) + }) }) it('should be able to read workspace role with READ_WORKSPACE_ROLE authority', async () => { @@ -741,7 +771,7 @@ describe('Workspace Role Controller Tests', () => { }) }) - it('should be able to add environment access for projects to the role with UPDATE_WORKSPACE_ROLE and READ_PROJECT authorities', async () => { + it('should be able to add environment access for projects to the role with READ_WORKSPACE, UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT authorities', async () => { const devEnvironment = await prisma.environment.create({ data: { name: 'dev', @@ -756,6 +786,7 @@ describe('Workspace Role Controller Tests', () => { projectId: projects[1].id } }) + // update the workspace member role with the required authorities await prisma.workspaceRole.update({ where: { workspaceId_name: { @@ -768,7 +799,34 @@ describe('Workspace Role Controller Tests', () => { set: [ Authority.UPDATE_WORKSPACE_ROLE, Authority.READ_PROJECT, - Authority.READ_WORKSPACE_ROLE + Authority.READ_WORKSPACE_ROLE, + Authority.READ_ENVIRONMENT + ] + }, + projects: { + create: [ + { + project: { + connect: { + id: projects[0].id + } + }, + environments: { + connect: { + id: devEnvironment.id + } + } + }, + { + project: { + connect: { + id: projects[1].id + } + }, + environments: { + connect: { id: stageEnvironment.id } + } + } ] } } @@ -793,7 +851,6 @@ describe('Workspace Role Controller Tests', () => { 'x-e2e-user-email': charlie.email } }) - expect(response.statusCode).toBe(200) expect(response.json()).toEqual({ ...adminRole1, @@ -881,6 +938,33 @@ describe('Workspace Role Controller Tests', () => { expect(response.statusCode).toBe(400) }) + + it('should not be able to update the workspace role with environments that the user does not have read access to', async () => { + await prisma.environment.create({ + data: { + name: 'dev', + slug: 'dev', + projectId: projects[0].id + } + }) + const response = await app.inject({ + method: 'PUT', + url: `/workspace-role/${adminRole1.slug}`, + payload: { + projectEnvironments: [ + { + projectSlug: projects[0].slug, + environmentSlugs: ['dev'] + } + ] + }, + headers: { + 'x-e2e-user-email': charlie.email + } + }) + + expect(response.statusCode).toBe(401) + }) }) describe('Delete Workspace Role Tests', () => { diff --git a/apps/api/src/workspace/workspace.e2e.spec.ts b/apps/api/src/workspace/workspace.e2e.spec.ts index 482c31084..0f0cd1f2c 100644 --- a/apps/api/src/workspace/workspace.e2e.spec.ts +++ b/apps/api/src/workspace/workspace.e2e.spec.ts @@ -756,9 +756,7 @@ describe('Workspace Controller Tests', () => { Authority.READ_VARIABLE, Authority.READ_WORKSPACE ], - projectEnvironments: [ - { projectSlug: project2Response.slug, environmentSlugs: [] } - ] + projectEnvironments: [{ projectSlug: project2Response.slug }] }) const project1DevEnv = await prisma.environment.findUnique({ diff --git a/apps/cli/src/commands/workspace/role/create.role.ts b/apps/cli/src/commands/workspace/role/create.role.ts index e94fce2e8..96b59a056 100644 --- a/apps/cli/src/commands/workspace/role/create.role.ts +++ b/apps/cli/src/commands/workspace/role/create.role.ts @@ -19,8 +19,8 @@ export default class UpdateRoleCommand extends BaseCommand { getArguments(): CommandArgument[] { return [ { - name: '', - description: 'Slug of the workspace role you want to fetch.' + name: '', + description: 'Slug of the workspace to associate this role.' } ] } @@ -62,7 +62,7 @@ export default class UpdateRoleCommand extends BaseCommand { } async action({ args, options }: CommandActionData): Promise { - const [workspaceRoleSlug] = args + const [workspaceSlug] = args const { name, description, @@ -76,28 +76,32 @@ export default class UpdateRoleCommand extends BaseCommand { const projectSlugsArray = projectSlugs?.split(',') const environmentSlugsArray = environmentSlugs?.split(',') - if (projectSlugsArray?.length !== environmentSlugsArray?.length) { + if ( + projectSlugsArray && + environmentSlugsArray && + projectSlugsArray?.length !== environmentSlugsArray?.length + ) { Logger.error('Number of projects and environments should be equal') return } const projectEnvironments: Array<{ projectSlug: string - environmentSlugs: string[] + environmentSlugs?: string[] }> = [] const len = projectSlugsArray.length for (let i = 0; i < len; i++) { projectEnvironments.push({ projectSlug: projectSlugsArray[i], - environmentSlugs: environmentSlugsArray[i].split(':') + environmentSlugs: environmentSlugsArray?.[i].split(':') }) } const { data, error, success } = - await ControllerInstance.getInstance().workspaceRoleController.updateWorkspaceRole( + await ControllerInstance.getInstance().workspaceRoleController.createWorkspaceRole( { - workspaceRoleSlug, + workspaceSlug, name, description, colorCode, diff --git a/packages/api-client/tests/workspace-role.spec.ts b/packages/api-client/tests/workspace-role.spec.ts index f2d218d2a..ab0e57c9e 100644 --- a/packages/api-client/tests/workspace-role.spec.ts +++ b/packages/api-client/tests/workspace-role.spec.ts @@ -64,7 +64,7 @@ describe('Workspace Role Controller Tests', () => { description: 'Role for developers', colorCode: '#FF0000', authorities: ['READ_WORKSPACE', 'READ_PROJECT'], - projectEnvironments: [{ projectSlug, environmentSlugs: [] }] + projectEnvironments: [{ projectSlug }] } const createWorkspaceRoleResponse = ( @@ -199,7 +199,7 @@ describe('Workspace Role Controller Tests', () => { description: 'Role with project access', colorCode: '#0000FF', authorities: ['READ_WORKSPACE'], - projectEnvironments: [{ projectSlug, environmentSlugs: [] }] + projectEnvironments: [{ projectSlug }] } const createRoleResponse = ( diff --git a/packages/schema/src/workspace-role/index.ts b/packages/schema/src/workspace-role/index.ts index ce34585e5..1a29786be 100644 --- a/packages/schema/src/workspace-role/index.ts +++ b/packages/schema/src/workspace-role/index.ts @@ -44,7 +44,7 @@ export const CreateWorkspaceRoleRequestSchema = z.object({ .array( z.object({ projectSlug: BaseProjectSchema.shape.slug, - environmentSlugs: z.array(EnvironmentSchema.shape.slug) + environmentSlugs: z.array(EnvironmentSchema.shape.slug).optional() }) ) .optional()