Skip to content

Commit

Permalink
feat(api) : Enhance permissions to allow filtering by environments th…
Browse files Browse the repository at this point in the history
…rough roles
  • Loading branch information
muntaxir4 committed Dec 28, 2024
1 parent a9fc39e commit 4047f1e
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 45 deletions.
5 changes: 3 additions & 2 deletions apps/api/src/common/authority-checker.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ProjectWithSecrets } from '@/project/project.types'
import { SecretWithProjectAndVersion } from '@/secret/secret.types'
import { CustomLoggerService } from './logger.service'
import {
getCollectiveEnvironmentAuthorities,
getCollectiveProjectAuthorities,
getCollectiveWorkspaceAuthorities
} from './collective-authorities'
Expand Down Expand Up @@ -221,9 +222,9 @@ export class AuthorityCheckerService {
throw new NotFoundException(`Environment ${entity.slug} not found`)
}

const permittedAuthorities = await getCollectiveProjectAuthorities(
const permittedAuthorities = await getCollectiveEnvironmentAuthorities(
userId,
environment.project,
environment,
prisma
)

Expand Down
73 changes: 73 additions & 0 deletions apps/api/src/common/collective-authorities.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EnvironmentWithProject } from '@/environment/environment.types'
import {
Authority,
PrismaClient,
Expand Down Expand Up @@ -93,3 +94,75 @@ export const getCollectiveProjectAuthorities = async (

return authorities
}

/**
* Given the userId and environment, this function returns the set of authorities
* that are formed by accumulating a set of all the authorities across all the
* roles that the user has in the workspace, adding an extra layer of filtering
* by the project and the environment.
* @param userId The id of the user
* @param environemnt The environment with the project
* @param prisma The prisma client
* @returns
*/
export const getCollectiveEnvironmentAuthorities = async (
userId: User['id'],
environemnt: EnvironmentWithProject,
prisma: PrismaClient
): Promise<Set<Authority>> => {
const authorities = new Set<Authority>()

const projectRoleAssociations =
await prisma.workspaceMemberRoleAssociation.findMany({
where: {
workspaceMember: {
userId,
workspaceId: environemnt.project.workspaceId
}
},
select: {
role: {
select: {
authorities: true
}
}
}
})

if (projectRoleAssociations.length === 0) {
return authorities
}

const environmentRoleAssociations =
await prisma.projectWorkspaceRoleAssociation.findMany({
where: {
projectId: environemnt.project.id,
environments: {
some: {
id: environemnt.id
}
}
},
select: {
role: {
select: {
authorities: true
}
}
}
})

projectRoleAssociations.forEach((roleAssociation) => {
roleAssociation.role.authorities.forEach((authority) => {
authorities.add(authority)
})
})

environmentRoleAssociations.forEach((roleAssociation) => {
roleAssociation.role.authorities.forEach((authority) => {
authorities.add(authority)
})
})

return authorities
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator'
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'

export class CreateEnvironment {
@IsString()
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/event/event.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,9 @@ describe('Event Controller Tests', () => {
description: 'Some description',
colorCode: '#000000',
authorities: [],
projectSlugs: [project.slug]
projectEnvironments: [
{ projectSlug: project.slug, environmentSlugs: [] }
]
}
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "_EnvironmentToProjectWorkspaceRoleAssociation" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,

CONSTRAINT "_EnvironmentToProjectWorkspaceRoleAssociation_AB_pkey" PRIMARY KEY ("A","B")
);

-- CreateIndex
CREATE INDEX "_EnvironmentToProjectWorkspaceRoleAssociation_B_index" ON "_EnvironmentToProjectWorkspaceRoleAssociation"("B");

-- AddForeignKey
ALTER TABLE "_EnvironmentToProjectWorkspaceRoleAssociation" ADD CONSTRAINT "_EnvironmentToProjectWorkspaceRoleAssociation_A_fkey" FOREIGN KEY ("A") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_EnvironmentToProjectWorkspaceRoleAssociation" ADD CONSTRAINT "_EnvironmentToProjectWorkspaceRoleAssociation_B_fkey" FOREIGN KEY ("B") REFERENCES "ProjectWorkspaceRoleAssociation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
2 changes: 1 addition & 1 deletion apps/api/src/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
4 changes: 4 additions & 0 deletions apps/api/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ model Environment {
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
projectId String
projectWorkspaceRoleAssociations ProjectWorkspaceRoleAssociation[]
@@unique([projectId, name])
@@index([name])
}
Expand Down Expand Up @@ -295,6 +297,8 @@ model ProjectWorkspaceRoleAssociation {
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
projectId String
environments Environment[]
@@unique([roleId, projectId])
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Authority } from '@prisma/client'
import { IsArray, IsOptional, IsString } from 'class-validator'
import {
IsArray,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested
} from 'class-validator'

export class CreateWorkspaceRole {
@IsString()
Expand All @@ -19,5 +25,16 @@ export class CreateWorkspaceRole {

@IsArray()
@IsOptional()
readonly projectSlugs?: string[]
@ValidateNested({ each: true })
readonly projectEnvironments?: ProjectEnvironments[]
}

class ProjectEnvironments {
@IsString()
@IsNotEmpty()
readonly projectSlug: string

@IsArray()
@IsNotEmpty({ each: true })
readonly environmentSlugs: string[]
}
118 changes: 89 additions & 29 deletions apps/api/src/workspace-role/service/workspace-role.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ export class WorkspaceRoleService {
data: {
id: workspaceRoleId,
name: dto.name,
slug: await generateEntitySlug(dto.name, 'API_KEY', this.prisma),
slug: await generateEntitySlug(
dto.name,
'WORKSPACE_ROLE',
this.prisma
),
description: dto.description,
colorCode: dto.colorCode,
authorities: dto.authorities ?? [],
Expand All @@ -94,6 +98,43 @@ export class WorkspaceRoleService {
}
}
},
select: {
id: true
}
})
)

if (dto.projectEnvironments) {
// Create the project associations
const projectSlugToIdMap = await this.getProjectSlugToIdMap(
dto.projectEnvironments.map((pe) => pe.projectSlug)
)

for (const pe of dto.projectEnvironments) {
const projectId = projectSlugToIdMap.get(pe.projectSlug)
if (projectId) {
// 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: {
connect: pe.environmentSlugs.map((slug) => ({ slug }))
}
}
})
)
}
}
}

// Fetch the new workspace role
op.push(
this.prisma.workspaceRole.findFirst({
where: {
id: workspaceRoleId
},
include: {
projects: {
select: {
Expand All @@ -103,32 +144,21 @@ export class WorkspaceRoleService {
slug: true,
name: true
}
},
environments: {
select: {
id: true,
slug: true,
name: true
}
}
}
}
}
})
)

if (dto.projectSlugs) {
// Create the project associations
const projectSlugToIdMap = await this.getProjectSlugToIdMap(
dto.projectSlugs
)

if (dto.projectSlugs && dto.projectSlugs.length > 0) {
op.push(
this.prisma.projectWorkspaceRoleAssociation.createMany({
data: dto.projectSlugs.map((projectSlug) => ({
roleId: workspaceRoleId,
projectId: projectSlugToIdMap.get(projectSlug)
}))
})
)
}
}

const workspaceRole = (await this.prisma.$transaction(op))[0]
const workspaceRole = (await this.prisma.$transaction(op)).pop()

await createEvent(
{
Expand Down Expand Up @@ -204,23 +234,44 @@ export class WorkspaceRoleService {
)
}

if (dto.projectSlugs) {
if (dto.projectEnvironments) {
await this.prisma.projectWorkspaceRoleAssociation.deleteMany({
where: {
roleId: workspaceRoleId
}
})

const projectSlugToIdMap = await this.getProjectSlugToIdMap(
dto.projectSlugs
dto.projectEnvironments.map((pe) => pe.projectSlug)
)

await this.prisma.projectWorkspaceRoleAssociation.createMany({
data: dto.projectSlugs.map((projectSlug) => ({
roleId: workspaceRoleId,
projectId: projectSlugToIdMap.get(projectSlug)
}))
})
for (const pe of dto.projectEnvironments) {
const projectId = projectSlugToIdMap.get(pe.projectSlug)
if (projectId) {
// Create or Update the project workspace role association with the environments accessible on the project
await this.prisma.projectWorkspaceRoleAssociation.upsert({
where: {
roleId_projectId: {
roleId: workspaceRoleId,
projectId: projectId
}
},
update: {
environments: {
set: [],
connect: pe.environmentSlugs.map((slug) => ({ slug }))
}
},
create: {
roleId: workspaceRoleId,
projectId: projectId,
environments: {
connect: pe.environmentSlugs.map((slug) => ({ slug }))
}
}
})
}
}
}

const updatedWorkspaceRole = await this.prisma.workspaceRole.update({
Expand All @@ -229,6 +280,9 @@ export class WorkspaceRoleService {
},
data: {
name: dto.name,
slug: dto.name
? await generateEntitySlug(dto.name, 'WORKSPACE_ROLE', this.prisma)
: undefined,
description: dto.description,
colorCode: dto.colorCode,
authorities: dto.authorities
Expand All @@ -242,12 +296,18 @@ export class WorkspaceRoleService {
slug: true,
name: true
}
},
environments: {
select: {
id: true,
slug: true,
name: true
}
}
}
}
}
})

await createEvent(
{
triggeredBy: user,
Expand Down
Loading

0 comments on commit 4047f1e

Please sign in to comment.