diff --git a/api-collection/Secret Controller/Fetch all by project and environment.bru b/api-collection/Secret Controller/Fetch all by project and environment.bru new file mode 100644 index 000000000..649df0499 --- /dev/null +++ b/api-collection/Secret Controller/Fetch all by project and environment.bru @@ -0,0 +1,26 @@ +meta { + name: Fetch all by project and environment + type: http + seq: 5 +} + +get { + url: {{BASE_URL}}/api/secret/:project_slug/:environment_slug + body: none + auth: bearer +} + +params:path { + project_slug: + environment_slug: +} + +auth:bearer { + token: {{JWT}} +} + +docs { + ## Description + + Fetches all the secrets for a particular pair of project and environment. Used by the CLI to prefetch the existing secrets. +} \ No newline at end of file diff --git a/api-collection/Variable Controller/Fetch all by project and environment.bru b/api-collection/Variable Controller/Fetch all by project and environment.bru new file mode 100644 index 000000000..6dce34152 --- /dev/null +++ b/api-collection/Variable Controller/Fetch all by project and environment.bru @@ -0,0 +1,26 @@ +meta { + name: Fetch all by project and environment + type: http + seq: 5 +} + +get { + url: {{BASE_URL}}/api/variable/:project_slug/:environment_slug + body: none + auth: bearer +} + +params:path { + project_slug: project-1-uzukc + environment_slug: alpha-l7xvp +} + +auth:bearer { + token: {{JWT}} +} + +docs { + ## Description + + Fetches all the variables for a particular pair of project and environment. Used by the CLI to prefetch the existing variables. +} \ No newline at end of file diff --git a/apps/api/jest.e2e-config.ts b/apps/api/jest.e2e-config.ts index 0f2006243..c853ca443 100644 --- a/apps/api/jest.e2e-config.ts +++ b/apps/api/jest.e2e-config.ts @@ -3,7 +3,8 @@ export default { forceExit: true, displayName: 'api', testEnvironment: 'node', - testMatch: ['**/*.e2e.spec.ts'], + testMatch: ['**/project.e2e.spec.ts'], + testTimeout: 10000, transform: { '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }] }, diff --git a/apps/api/src/auth/service/authorization.service.ts b/apps/api/src/auth/service/authorization.service.ts index 0eb439dd7..ddb81c5a1 100644 --- a/apps/api/src/auth/service/authorization.service.ts +++ b/apps/api/src/auth/service/authorization.service.ts @@ -7,7 +7,7 @@ import { SecretWithProjectAndVersion } from '@/secret/secret.types' import { IntegrationWithWorkspace } from '@/integration/integration.types' import { AuthorizationParams } from './authorization.types' import { AuthenticatedUser } from '@/user/user.types' -import { Workspace, User } from '@prisma/client' +import { Workspace } from '@prisma/client' import { PrismaService } from '@/prisma/prisma.service' import { CustomLoggerService } from '@/common/logger.service' import { InternalServerErrorException, NotFoundException } from '@nestjs/common' @@ -52,16 +52,24 @@ export class AuthorizationService { public async authorizeUserAccessToProject( params: AuthorizationParams ): Promise<ProjectWithSecrets> { + console.log( + `${Date.now()}: authorizeUserAccessToProject ${params.entity.slug} - started` + ) const project = await this.authorityCheckerService.checkAuthorityOverProject(params) - const workspace = await this.getWorkspace( - params.user.id, - project.workspaceId + console.log( + `${Date.now()}: Project acces authorized ${project.name} WorkspaceId: ${project.workspaceId}` ) + const workspace = await this.getWorkspace(project.workspaceId) + this.checkUserHasAccessToWorkspace(params.user, workspace) + console.log( + `${Date.now()}: authorizeUserAccessToProject ${params.entity.slug} - finished` + ) + return project } @@ -80,10 +88,7 @@ export class AuthorizationService { const environment = await this.authorityCheckerService.checkAuthorityOverEnvironment(params) - const workspace = await this.getWorkspace( - params.user.id, - environment.project.workspaceId - ) + const workspace = await this.getWorkspace(environment.project.workspaceId) this.checkUserHasAccessToWorkspace(params.user, workspace) @@ -105,10 +110,7 @@ export class AuthorizationService { const variable = await this.authorityCheckerService.checkAuthorityOverVariable(params) - const workspace = await this.getWorkspace( - params.user.id, - variable.project.workspaceId - ) + const workspace = await this.getWorkspace(variable.project.workspaceId) this.checkUserHasAccessToWorkspace(params.user, workspace) @@ -130,10 +132,7 @@ export class AuthorizationService { const secret = await this.authorityCheckerService.checkAuthorityOverSecret(params) - const workspace = await this.getWorkspace( - params.user.id, - secret.project.workspaceId - ) + const workspace = await this.getWorkspace(secret.project.workspaceId) this.checkUserHasAccessToWorkspace(params.user, workspace) @@ -162,16 +161,12 @@ export class AuthorizationService { /** * Fetches the requested workspace specified by userId and the filter. - * @param userId The id of the user - * @param filter The filter optionally including the workspace id, slug or name + * @param workspaceId The workspace id * @returns The requested workspace * @throws InternalServerErrorException if there's an error when communicating with the database * @throws NotFoundException if the workspace is not found */ - private async getWorkspace( - userId: User['id'], - workspaceId: Workspace['id'] - ): Promise<Workspace> { + private async getWorkspace(workspaceId: Workspace['id']): Promise<Workspace> { let workspace: Workspace try { @@ -186,6 +181,15 @@ export class AuthorizationService { } if (!workspace) { + console.log(`${Date.now()}: Workspace not found: ${workspaceId}`) + + const workspaces = await this.prisma.workspace.findMany() + + console.log(`${Date.now()}: Workspaces: ${workspaces.length}`) + for (const workspace of workspaces) { + console.log(`${workspace.name}: ${workspace.id}`) + } + throw new NotFoundException(`Workspace ${workspaceId} not found`) } diff --git a/apps/api/src/project/project.e2e.spec.ts b/apps/api/src/project/project.e2e.spec.ts index b984dd623..5cfd75f98 100644 --- a/apps/api/src/project/project.e2e.spec.ts +++ b/apps/api/src/project/project.e2e.spec.ts @@ -101,6 +101,7 @@ describe('Project Controller Tests', () => { }) beforeEach(async () => { + console.log(`${Date.now()}: Set up test data`) const createUser1 = await userService.createUser({ name: 'John Doe', email: 'johndoe@keyshade.xyz', @@ -120,6 +121,9 @@ describe('Project Controller Tests', () => { workspace1 = createUser1.defaultWorkspace workspace2 = createUser2.defaultWorkspace + console.log(`${Date.now()}: Workspace 1: ${workspace1.id}`) + console.log(`${Date.now()}: Workspace 2: ${workspace2.id}`) + delete createUser1.defaultWorkspace delete createUser2.defaultWorkspace @@ -151,11 +155,16 @@ describe('Project Controller Tests', () => { 'Project for testing if all environments,secrets and keys are being fetched or not', storePrivateKey: true })) as Project + console.log(`${Date.now()}: Test data set up`) }) afterEach(async () => { - await prisma.user.deleteMany() - await prisma.workspace.deleteMany() + console.log(`${Date.now()}: Cleaning up test data`) + await prisma.$transaction([ + prisma.user.deleteMany(), + prisma.workspace.deleteMany() + ]) + console.log(`${Date.now()}: Test data cleaned up`) }) it('should be defined', async () => { @@ -1126,12 +1135,12 @@ describe('Project Controller Tests', () => { user2, project3.slug, { - name: 'Forked Project' + name: 'Forked Project 1' } )) as Project expect(forkedProject).toBeDefined() - expect(forkedProject.name).toBe('Forked Project') + expect(forkedProject.name).toBe('Forked Project 1') expect(forkedProject.publicKey).toBeDefined() expect(forkedProject.privateKey).toBeDefined() expect(forkedProject.publicKey).not.toBe(project3.publicKey) @@ -1147,7 +1156,7 @@ describe('Project Controller Tests', () => { }) expect(forkedProjectFromDB).toBeDefined() - expect(forkedProjectFromDB.name).toBe('Forked Project') + expect(forkedProjectFromDB.name).toBe('Forked Project 1') expect(forkedProjectFromDB.publicKey).toBeDefined() expect(forkedProjectFromDB.privateKey).toBeDefined() expect(forkedProjectFromDB.publicKey).not.toBe(project3.publicKey) @@ -1162,7 +1171,7 @@ describe('Project Controller Tests', () => { method: 'POST', url: `/project/123/fork`, payload: { - name: 'Forked Project' + name: 'Forked Project 2' }, headers: { 'x-e2e-user-email': user2.email @@ -1177,7 +1186,7 @@ describe('Project Controller Tests', () => { method: 'POST', url: `/project/${project2.slug}/fork`, payload: { - name: 'Forked Project' + name: 'Forked Project 3' }, headers: { 'x-e2e-user-email': user1.email @@ -1192,7 +1201,7 @@ describe('Project Controller Tests', () => { user2, project3.slug, { - name: 'Forked Project' + name: 'Forked Project 4' } )) as Project @@ -1208,7 +1217,7 @@ describe('Project Controller Tests', () => { user2, project3.slug, { - name: 'Forked Project', + name: 'Forked Project 5', workspaceSlug: newWorkspace.slug } )) as Project @@ -1218,7 +1227,7 @@ describe('Project Controller Tests', () => { it('should not be able to create a fork with the same name in a workspace', async () => { await projectService.createProject(user2, workspace2.slug, { - name: 'Forked Project', + name: 'Forked Project 6', description: 'Forked Project description', storePrivateKey: true, accessLevel: ProjectAccessLevel.GLOBAL @@ -1228,7 +1237,7 @@ describe('Project Controller Tests', () => { method: 'POST', url: `/project/${project3.slug}/fork`, payload: { - name: 'Forked Project' + name: 'Forked Project 6' }, headers: { 'x-e2e-user-email': user2.email @@ -1319,7 +1328,7 @@ describe('Project Controller Tests', () => { user2, project3.slug, { - name: 'Forked Project' + name: 'Forked Project 8' } ) @@ -1446,7 +1455,7 @@ describe('Project Controller Tests', () => { user2, project3.slug, { - name: 'Forked Project' + name: 'Forked Project 9' } ) @@ -1636,7 +1645,7 @@ describe('Project Controller Tests', () => { user2, project3.slug, { - name: 'Forked Project' + name: 'Forked Project 10' } ) @@ -1771,7 +1780,7 @@ describe('Project Controller Tests', () => { user2, project3.slug, { - name: 'Forked Project' + name: 'Forked Project 11' } ) @@ -1798,7 +1807,7 @@ describe('Project Controller Tests', () => { it('should be able to fetch all forked projects of a project', async () => { await projectService.forkProject(user2, project3.slug, { - name: 'Forked Project' + name: 'Forked Project 12' }) const response = await app.inject({ @@ -1827,33 +1836,57 @@ describe('Project Controller Tests', () => { }) it('should not contain a forked project that has access level other than GLOBAL', async () => { - // Make a hidden fork - const hiddenProject = await projectService.forkProject( - user2, - project3.slug, - { - name: 'Hidden Forked Project' - } - ) - await projectService.updateProject(user2, hiddenProject.slug, { - accessLevel: ProjectAccessLevel.INTERNAL - }) + try { + console.log(`${Date.now()}: Test started`) + console.log( + `${Date.now()}: Parent WorkspaceId: ${project3.workspaceId}` + ) + // Make a hidden fork + const hiddenProject = await projectService.forkProject( + user2, + project3.slug, + { + name: 'Hidden Forked Project' + } + ) + await projectService.updateProject(user2, hiddenProject.slug, { + accessLevel: ProjectAccessLevel.INTERNAL + }) - // Make a public fork - await projectService.forkProject(user2, project3.slug, { - name: 'Forked Project' - }) + console.log( + `${Date.now()}: Internal fork WorkspaceId: ${hiddenProject.workspaceId}` + ) - const response = await app.inject({ - method: 'GET', - url: `/project/${project3.slug}/forks`, - headers: { - 'x-e2e-user-email': user1.email - } - }) + // Make a public fork + const publicProject = await projectService.forkProject( + user2, + project3.slug, + { + name: 'Forked Project 13' + } + ) - expect(response.statusCode).toBe(200) - expect(response.json().items).toHaveLength(1) + console.log( + `${Date.now()}: Public fork WorkspaceId: ${publicProject.workspaceId}` + ) + + const response = await app.inject({ + method: 'GET', + url: `/project/${project3.slug}/forks`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + console.log(`${Date.now()}: Get forks response: ${response.json()}`) + + expect(response.statusCode).toBe(200) + expect(response.json().items).toHaveLength(1) + console.log(`${Date.now()}: Test finished`) + } catch (error) { + console.error(`${Date.now()}: Error: ${error}`) + throw error + } }) }) }) diff --git a/apps/api/src/project/service/project.service.ts b/apps/api/src/project/service/project.service.ts index 7db44a553..1686569c4 100644 --- a/apps/api/src/project/service/project.service.ts +++ b/apps/api/src/project/service/project.service.ts @@ -231,6 +231,7 @@ export class ProjectService { projectSlug: Project['slug'], dto: UpdateProject ) { + console.log(`${Date.now()}: updateProject ${projectSlug} - started`) // Check if the user has the authority to update the project let authority: Authority = Authority.UPDATE_PROJECT @@ -371,6 +372,7 @@ export class ProjectService { ) this.log.debug(`Updated project ${updatedProject.id}`) + console.log(`${Date.now()}: updateProject ${projectSlug} - finished`) return { ...updatedProject, privateKey, @@ -394,6 +396,7 @@ export class ProjectService { projectSlug: Project['slug'], forkMetadata: ForkProject ) { + console.log(`${Date.now()}: forkProject ${forkMetadata.name} - started`) const project = await this.authorizationService.authorizeUserAccessToProject({ user, @@ -462,7 +465,7 @@ export class ProjectService { accessLevel: project.accessLevel, isForked: true, forkedFromId: project.id, - workspaceId: workspaceId, + workspaceId, lastUpdatedById: userId } }) @@ -523,6 +526,7 @@ export class ProjectService { ) this.log.debug(`Created project ${newProject}`) + console.log(`${Date.now()}: forkProject ${forkMetadata.name} - finished`) return newProject } @@ -700,6 +704,8 @@ export class ProjectService { page: number, limit: number ) { + console.log(`${Date.now()}: getAllProjectForks - started`) + const project = await this.authorizationService.authorizeUserAccessToProject({ user, @@ -708,22 +714,33 @@ export class ProjectService { }) const projectId = project.id + console.log( + `${Date.now()}: Parent Project WorkspaceId: ${project.workspaceId}` + ) + const forks = await this.prisma.project.findMany({ where: { forkedFromId: projectId } }) - const forksAllowed = forks.filter(async (fork) => { - const allowed = - (await this.authorizationService.authorizeUserAccessToProject({ - user, - entity: { slug: fork.slug }, - authorities: [Authority.READ_PROJECT] - })) != null - - return allowed - }) + const forksAllowed = await Promise.all( + forks.map(async (fork) => { + console.log( + `${Date.now()}: Fork ${fork.name} Project WorkspaceId: ${fork.workspaceId}` + ) + const allowed = + (await this.authorizationService.authorizeUserAccessToProject({ + user, + entity: { slug: fork.slug }, + authorities: [Authority.READ_PROJECT] + })) != null + + return { fork, allowed } + }) + ).then((results) => + results.filter((result) => result.allowed).map((result) => result.fork) + ) const items = forksAllowed.slice(page * limit, (page + 1) * limit) @@ -737,6 +754,8 @@ export class ProjectService { } ) + console.log(`${Date.now()}: getAllProjectForks - finished`) + return { items, metadata } } diff --git a/apps/api/src/secret/controller/secret.controller.ts b/apps/api/src/secret/controller/secret.controller.ts index 7f3e757eb..b4e0e12da 100644 --- a/apps/api/src/secret/controller/secret.controller.ts +++ b/apps/api/src/secret/controller/secret.controller.ts @@ -110,4 +110,18 @@ export class SecretController { order ) } + + @Get('/:projectSlug/:environmentSlug') + @RequiredApiKeyAuthorities(Authority.READ_SECRET) + async getAllSecretsOfEnvironment( + @CurrentUser() user: AuthenticatedUser, + @Param('projectSlug') projectSlug: string, + @Param('environmentSlug') environmentSlug: string + ) { + return await this.secretService.getAllSecretsOfProjectAndEnvironment( + user, + projectSlug, + environmentSlug + ) + } } diff --git a/apps/api/src/secret/secret.e2e.spec.ts b/apps/api/src/secret/secret.e2e.spec.ts index 917c0f25a..00370e32f 100644 --- a/apps/api/src/secret/secret.e2e.spec.ts +++ b/apps/api/src/secret/secret.e2e.spec.ts @@ -1160,4 +1160,102 @@ describe('Secret Controller Tests', () => { expect(event.title).toBe('Secret rotated') }) }) + + describe('Fetch All Secrets By Project And Environment Tests', () => { + it('should be able to fetch all secrets by project and environment', async () => { + const response = await app.inject({ + method: 'GET', + url: `/secret/${project1.slug}/${environment1.slug}`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json().length).toBe(1) + + const secret = response.json()[0] + expect(secret.name).toBe('Secret 1') + expect(secret.value).toBe('Secret 1 value') + expect(secret.isPlaintext).toBe(true) + }) + + it('should not be able to fetch all secrets by project and environment if project does not exists', async () => { + const response = await app.inject({ + method: 'GET', + url: `/secret/non-existing-project-slug/${environment1.slug}`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(404) + }) + + it('should not be able to fetch all secrets by project and environment if environment does not exists', async () => { + const response = await app.inject({ + method: 'GET', + url: `/secret/${project1.slug}/non-existing-environment-slug`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(404) + }) + + it('should not be able to fetch all secrets by project and environment if the user has no access to the project', async () => { + const response = await app.inject({ + method: 'GET', + url: `/secret/${project1.slug}/${environment1.slug}`, + headers: { + 'x-e2e-user-email': user2.email + } + }) + + expect(response.statusCode).toBe(401) + }) + + it('should not be sending the plaintext secret if project does not store the private key', async () => { + // Get the first environment of project 2 + const environment = await prisma.environment.findFirst({ + where: { + projectId: project2.id + } + }) + + // Create a secret in project 2 + await secretService.createSecret( + user1, + { + name: 'Secret 20', + entries: [ + { + environmentSlug: environment.slug, + value: 'Secret 20 value' + } + ], + rotateAfter: '24', + note: 'Secret 20 note' + }, + project2.slug + ) + + const response = await app.inject({ + method: 'GET', + url: `/secret/${project2.slug}/${environment.slug}`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json().length).toBe(1) + + const secret = response.json()[0] + expect(secret.name).toBe('Secret 20') + expect(secret.value).not.toBe('Secret 20 value') + expect(secret.isPlaintext).toBe(false) + }) + }) }) diff --git a/apps/api/src/secret/service/secret.service.ts b/apps/api/src/secret/service/secret.service.ts index caf87540f..447a68fc1 100644 --- a/apps/api/src/secret/service/secret.service.ts +++ b/apps/api/src/secret/service/secret.service.ts @@ -24,7 +24,10 @@ import { AuthorizationService } from '@/auth/service/authorization.service' import { RedisClientType } from 'redis' import { REDIS_CLIENT } from '@/provider/redis.provider' import { CHANGE_NOTIFIER_RSC } from '@/socket/change-notifier.socket' -import { ChangeNotificationEvent } from 'src/socket/socket.types' +import { + ChangeNotification, + ChangeNotificationEvent +} from '@/socket/socket.types' import { paginate } from '@/common/paginate' import { addHoursToDate, @@ -770,6 +773,89 @@ export class SecretService { return { items, metadata } } + /** + * Gets all secrets of a project and environment + * @param user the user performing the action + * @param projectSlug the slug of the project + * @param environmentSlug the slug of the environment + * @returns an array of objects with the secret name and value + * @throws {NotFoundException} if the project or environment does not exist + * @throws {BadRequestException} if the user does not have the required role + */ + async getAllSecretsOfProjectAndEnvironment( + user: AuthenticatedUser, + projectSlug: Project['slug'], + environmentSlug: Environment['slug'] + ) { + // Fetch the project + const project = + await this.authorizationService.authorizeUserAccessToProject({ + user, + entity: { slug: projectSlug }, + authorities: [Authority.READ_SECRET] + }) + const projectId = project.id + + // Check access to the environment + const environment = + await this.authorizationService.authorizeUserAccessToEnvironment({ + user, + entity: { slug: environmentSlug }, + authorities: [Authority.READ_ENVIRONMENT] + }) + const environmentId = environment.id + + const secrets = await this.prisma.secret.findMany({ + where: { + projectId, + versions: { + some: { + environmentId + } + } + }, + include: { + lastUpdatedBy: { + select: { + id: true, + name: true + } + }, + versions: { + where: { + environmentId + }, + orderBy: { + version: 'desc' + }, + take: 1, + include: { + environment: { + select: { + id: true, + slug: true + } + } + } + } + } + }) + + const response: ChangeNotification[] = [] + + for (const secret of secrets) { + response.push({ + name: secret.name, + value: project.storePrivateKey + ? await decrypt(project.privateKey, secret.versions[0].value) + : secret.versions[0].value, + isPlaintext: project.storePrivateKey + }) + } + + return response + } + /** * Rotate values of secrets that have reached their rotation time * @param currentTime the current time diff --git a/apps/api/src/variable/controller/variable.controller.ts b/apps/api/src/variable/controller/variable.controller.ts index 1a15d29d7..fc967cbd4 100644 --- a/apps/api/src/variable/controller/variable.controller.ts +++ b/apps/api/src/variable/controller/variable.controller.ts @@ -106,4 +106,18 @@ export class VariableController { order ) } + + @Get('/:projectSlug/:environmentSlug') + @RequiredApiKeyAuthorities(Authority.READ_VARIABLE) + async getAllVariablesOfEnvironment( + @CurrentUser() user: AuthenticatedUser, + @Param('projectSlug') projectSlug: string, + @Param('environmentSlug') environmentSlug: string + ) { + return await this.variableService.getAllVariablesOfProjectAndEnvironment( + user, + projectSlug, + environmentSlug + ) + } } diff --git a/apps/api/src/variable/service/variable.service.ts b/apps/api/src/variable/service/variable.service.ts index d54e48e9a..0abdcc557 100644 --- a/apps/api/src/variable/service/variable.service.ts +++ b/apps/api/src/variable/service/variable.service.ts @@ -22,7 +22,10 @@ import { RedisClientType } from 'redis' import { REDIS_CLIENT } from '@/provider/redis.provider' import { CHANGE_NOTIFIER_RSC } from '@/socket/change-notifier.socket' import { AuthorizationService } from '@/auth/service/authorization.service' -import { ChangeNotificationEvent } from 'src/socket/socket.types' +import { + ChangeNotification, + ChangeNotificationEvent +} from '@/socket/socket.types' import { paginate } from '@/common/paginate' import { getEnvironmentIdToSlugMap } from '@/common/environment' import generateEntitySlug from '@/common/slug-generator' @@ -715,6 +718,83 @@ export class VariableService { return { items, metadata } } + /** + * Gets all variables of a project and environment. + * @param user the user performing the action + * @param projectSlug the slug of the project to get the variables from + * @param environmentSlug the slug of the environment to get the variables from + * @returns an array of objects containing the name, value and whether the value is a plaintext + * @throws `NotFoundException` if the project or environment does not exist + * @throws `ForbiddenException` if the user does not have the required authority + */ + async getAllVariablesOfProjectAndEnvironment( + user: AuthenticatedUser, + projectSlug: Project['slug'], + environmentSlug: Environment['slug'] + ) { + // Check if the user has the required authorities in the project + const { id: projectId } = + await this.authorizationService.authorizeUserAccessToProject({ + user, + entity: { slug: projectSlug }, + authorities: [Authority.READ_VARIABLE] + }) + + // Check if the user has the required authorities in the environment + const { id: environmentId } = + await this.authorizationService.authorizeUserAccessToEnvironment({ + user, + entity: { slug: environmentSlug }, + authorities: [Authority.READ_ENVIRONMENT] + }) + + const variables = await this.prisma.variable.findMany({ + where: { + projectId, + versions: { + some: { + environmentId + } + } + }, + include: { + lastUpdatedBy: { + select: { + id: true, + name: true + } + }, + versions: { + where: { + environmentId + }, + select: { + value: true, + environment: { + select: { + id: true, + slug: true + } + } + }, + orderBy: { + version: 'desc' + }, + take: 1 + } + } + }) + + return variables.map( + (variable) => + ({ + name: variable.name, + value: variable.versions[0].value, + isPlaintext: true + }) as ChangeNotification + ) + } + /** * Checks if a variable with a given name already exists in a project. * Throws a ConflictException if the variable already exists. diff --git a/apps/api/src/variable/variable.e2e.spec.ts b/apps/api/src/variable/variable.e2e.spec.ts index 70872af83..b17e963a9 100644 --- a/apps/api/src/variable/variable.e2e.spec.ts +++ b/apps/api/src/variable/variable.e2e.spec.ts @@ -932,4 +932,60 @@ describe('Variable Controller Tests', () => { expect(response.statusCode).toBe(401) }) }) + + describe('Get All Variables By Project And Environment Tests', () => { + it('should be able to fetch all variables by project and environment', async () => { + const response = await app.inject({ + method: 'GET', + url: `/variable/${project1.slug}/${environment1.slug}`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json().length).toBe(1) + + const variable = response.json()[0] + expect(variable.name).toBe('Variable 1') + expect(variable.value).toBe('Variable 1 value') + expect(variable.isPlaintext).toBe(true) + }) + + it('should not be able to fetch all variables by project and environment if the user has no access to the project', async () => { + const response = await app.inject({ + method: 'GET', + url: `/variable/${project1.slug}/${environment1.slug}`, + headers: { + 'x-e2e-user-email': user2.email + } + }) + + expect(response.statusCode).toBe(401) + }) + + it('should not be able to fetch all variables by project and environment if the project does not exist', async () => { + const response = await app.inject({ + method: 'GET', + url: `/variable/non-existing-project-slug/${environment1.slug}`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(404) + }) + + it('should not be able to fetch all variables by project and environment if the environment does not exist', async () => { + const response = await app.inject({ + method: 'GET', + url: `/variable/${project1.slug}/non-existing-environment-slug`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(404) + }) + }) }) diff --git a/apps/cli/src/commands/run.command.ts b/apps/cli/src/commands/run.command.ts index 43bdfec63..5b624543d 100644 --- a/apps/cli/src/commands/run.command.ts +++ b/apps/cli/src/commands/run.command.ts @@ -160,9 +160,10 @@ export default class RunCommand extends BaseCommand { } if (childProcess === null) { childProcess = spawn(command, { + // @ts-expect-error this just works stdio: ['inherit', 'pipe', 'pipe'], shell: true, - env: this.processEnvironmentalVariables, + env: { ...process.env, ...this.processEnvironmentalVariables }, detached: true }) diff --git a/packages/api-client/src/controllers/secret.ts b/packages/api-client/src/controllers/secret.ts index a4d961d9c..176f1cd70 100644 --- a/packages/api-client/src/controllers/secret.ts +++ b/packages/api-client/src/controllers/secret.ts @@ -1,5 +1,9 @@ import { APIClient } from '@api-client/core/client' -import { ClientResponse } from '@keyshade/schema' +import { + ClientResponse, + GetAllSecretsOfEnvironmentRequest, + GetAllSecretsOfEnvironmentResponse +} from '@keyshade/schema' import { parseResponse } from '@api-client/core/response-parser' import { CreateSecretRequest, @@ -106,4 +110,14 @@ export default class SecretController { return await parseResponse<GetRevisionsOfSecretResponse>(response) } + + async getAllSecretsOfEnvironment( + request: GetAllSecretsOfEnvironmentRequest, + headers?: Record<string, string> + ): Promise<ClientResponse<GetAllSecretsOfEnvironmentResponse>> { + const url = `/api/secret/${request.projectSlug}/${request.environmentSlug}` + const response = await this.apiClient.get(url, headers) + + return await parseResponse<GetAllSecretsOfEnvironmentResponse>(response) + } } diff --git a/packages/api-client/src/controllers/variable.ts b/packages/api-client/src/controllers/variable.ts index de60d6eaa..d6ed0b23d 100644 --- a/packages/api-client/src/controllers/variable.ts +++ b/packages/api-client/src/controllers/variable.ts @@ -1,7 +1,11 @@ import { APIClient } from '@api-client/core/client' import { parsePaginationUrl } from '@api-client/core/pagination-parser' import { parseResponse } from '@api-client/core/response-parser' -import { ClientResponse } from '@keyshade/schema' +import { + ClientResponse, + GetAllVariablesOfEnvironmentRequest, + GetAllVariablesOfEnvironmentResponse +} from '@keyshade/schema' import { CreateVariableRequest, CreateVariableResponse, @@ -99,4 +103,14 @@ export default class VariableController { return await parseResponse<GetRevisionsOfVariableResponse>(response) } + + async getAllVariablesOfEnvironment( + request: GetAllVariablesOfEnvironmentRequest, + headers: Record<string, string> + ): Promise<ClientResponse<GetAllVariablesOfEnvironmentResponse>> { + const url = `/api/variable/${request.projectSlug}/${request.environmentSlug}` + const response = await this.apiClient.get(url, headers) + + return await parseResponse<GetAllVariablesOfEnvironmentResponse>(response) + } } diff --git a/packages/schema/src/secret/index.ts b/packages/schema/src/secret/index.ts index e075354ba..0b1879d38 100644 --- a/packages/schema/src/secret/index.ts +++ b/packages/schema/src/secret/index.ts @@ -129,3 +129,16 @@ export const GetRevisionsOfSecretResponseSchema = PageResponseSchema( }) }) ) + +export const GetAllSecretsOfEnvironmentRequestSchema = z.object({ + projectSlug: BaseProjectSchema.shape.slug, + environmentSlug: EnvironmentSchema.shape.slug +}) + +export const GetAllSecretsOfEnvironmentResponseSchema = z.array( + z.object({ + name: z.string(), + value: z.string(), + isPlaintext: z.boolean() + }) +) diff --git a/packages/schema/src/secret/index.types.ts b/packages/schema/src/secret/index.types.ts index 43920ad29..990bf9cf7 100644 --- a/packages/schema/src/secret/index.types.ts +++ b/packages/schema/src/secret/index.types.ts @@ -12,7 +12,9 @@ import { GetAllSecretsOfProjectRequestSchema, GetAllSecretsOfProjectResponseSchema, GetRevisionsOfSecretRequestSchema, - GetRevisionsOfSecretResponseSchema + GetRevisionsOfSecretResponseSchema, + GetAllSecretsOfEnvironmentRequestSchema, + GetAllSecretsOfEnvironmentResponseSchema } from '.' export type Secret = z.infer<typeof SecretSchema> @@ -50,3 +52,11 @@ export type GetRevisionsOfSecretRequest = z.infer< export type GetRevisionsOfSecretResponse = z.infer< typeof GetRevisionsOfSecretResponseSchema > + +export type GetAllSecretsOfEnvironmentRequest = z.infer< + typeof GetAllSecretsOfEnvironmentRequestSchema +> + +export type GetAllSecretsOfEnvironmentResponse = z.infer< + typeof GetAllSecretsOfEnvironmentResponseSchema +> diff --git a/packages/schema/src/variable/index.ts b/packages/schema/src/variable/index.ts index 810e5ba2a..d127db4bc 100644 --- a/packages/schema/src/variable/index.ts +++ b/packages/schema/src/variable/index.ts @@ -133,3 +133,16 @@ export const GetRevisionsOfVariableResponseSchema = PageResponseSchema( }) }) ) + +export const GetAllVariablesOfEnvironmentRequestSchema = z.object({ + projectSlug: BaseProjectSchema.shape.slug, + environmentSlug: EnvironmentSchema.shape.slug +}) + +export const GetAllVariablesOfEnvironmentResponseSchema = z.array( + z.object({ + name: z.string(), + value: z.string(), + isPlaintext: z.boolean() + }) +) diff --git a/packages/schema/src/variable/index.types.ts b/packages/schema/src/variable/index.types.ts index 609fbb24f..6bf88352b 100644 --- a/packages/schema/src/variable/index.types.ts +++ b/packages/schema/src/variable/index.types.ts @@ -12,7 +12,9 @@ import { GetAllVariablesOfProjectRequestSchema, GetAllVariablesOfProjectResponseSchema, GetRevisionsOfVariableRequestSchema, - GetRevisionsOfVariableResponseSchema + GetRevisionsOfVariableResponseSchema, + GetAllVariablesOfEnvironmentRequestSchema, + GetAllVariablesOfEnvironmentResponseSchema } from '.' export type Variable = z.infer<typeof VariableSchema> @@ -58,3 +60,11 @@ export type GetRevisionsOfVariableRequest = z.infer< export type GetRevisionsOfVariableResponse = z.infer< typeof GetRevisionsOfVariableResponseSchema > + +export type GetAllVariablesOfEnvironmentRequest = z.infer< + typeof GetAllVariablesOfEnvironmentRequestSchema +> + +export type GetAllVariablesOfEnvironmentResponse = z.infer< + typeof GetAllVariablesOfEnvironmentResponseSchema +>