diff --git a/.github/workflows/build-patroni-docker-img.yml b/.github/workflows/build-patroni-docker-img.yml new file mode 100644 index 000000000..4ee99f27a --- /dev/null +++ b/.github/workflows/build-patroni-docker-img.yml @@ -0,0 +1,38 @@ +name: Build patroni +on: [workflow_dispatch] + +jobs: + patroni-image: + name: Build patroni + runs-on: ubuntu-latest + steps: + - name: ☁️ Checkout source + uses: actions/checkout@v3 + with: + repository: 'zalando/patroni' + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 🏗️ Build backend and push to docker hub + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: . + file: Dockerfile + tags: | + infisical/patroni:${{ steps.commit.outputs.short }} + infisical/patroni:latest + platforms: linux/amd64,linux/arm64 + + \ No newline at end of file diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 40137a455..e4a5945e0 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -1,6 +1,8 @@ name: Build and release CLI on: + workflow_dispatch: + push: # run only against tags tags: @@ -14,6 +16,12 @@ jobs: cli-integration-tests: name: Run tests before deployment uses: ./.github/workflows/run-cli-tests.yml + secrets: + CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }} + CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }} + CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }} + CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }} + CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} goreleaser: runs-on: ubuntu-20.04 diff --git a/.github/workflows/run-cli-tests.yml b/.github/workflows/run-cli-tests.yml index 846998528..e814f9143 100644 --- a/.github/workflows/run-cli-tests.yml +++ b/.github/workflows/run-cli-tests.yml @@ -6,7 +6,20 @@ on: paths: - "cli/**" + workflow_dispatch: + workflow_call: + secrets: + CLI_TESTS_UA_CLIENT_ID: + required: true + CLI_TESTS_UA_CLIENT_SECRET: + required: true + CLI_TESTS_SERVICE_TOKEN: + required: true + CLI_TESTS_PROJECT_ID: + required: true + CLI_TESTS_ENV_SLUG: + required: true jobs: test: diff --git a/backend/e2e-test/routes/v3/secrets.spec.ts b/backend/e2e-test/routes/v3/secrets.spec.ts index 03e1c2f50..e7e271279 100644 --- a/backend/e2e-test/routes/v3/secrets.spec.ts +++ b/backend/e2e-test/routes/v3/secrets.spec.ts @@ -942,6 +942,113 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }] const secrets = await getSecrets(seedData1.environment.slug, path); expect(secrets).toEqual([]); }); + + test.each(testRawSecrets)("Bulk create secret raw in path $path", async ({ path, secret }) => { + const createSecretReqBody = { + projectSlug: seedData1.project.slug, + environment: seedData1.environment.slug, + secretPath: path, + secrets: [ + { + secretKey: secret.key, + secretValue: secret.value, + secretComment: secret.comment + } + ] + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/batch/raw`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secrets"); + + // fetch secrets + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: secret.value, + type: SecretType.Shared + }) + ]) + ); + + await deleteRawSecret({ path, key: secret.key }); + }); + + test.each(testRawSecrets)("Bulk update secret raw in path $path", async ({ secret, path }) => { + await createRawSecret({ path, ...secret }); + const updateSecretReqBody = { + projectSlug: seedData1.project.slug, + environment: seedData1.environment.slug, + secretPath: path, + secrets: [ + { + secretValue: "new-value", + secretKey: secret.key + } + ] + }; + const updateSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/batch/raw`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: updateSecretReqBody + }); + expect(updateSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(updateSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secrets"); + + // fetch secrets + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: "new-value", + version: 2, + type: SecretType.Shared + }) + ]) + ); + + await deleteRawSecret({ path, key: secret.key }); + }); + + test.each(testRawSecrets)("Bulk delete secret raw in path $path", async ({ path, secret }) => { + await createRawSecret({ path, ...secret }); + + const deletedSecretReqBody = { + projectSlug: seedData1.project.slug, + environment: seedData1.environment.slug, + secretPath: path, + secrets: [{ secretKey: secret.key }] + }; + const deletedSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/batch/raw`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: deletedSecretReqBody + }); + expect(deletedSecRes.statusCode).toBe(200); + const deletedSecretPayload = JSON.parse(deletedSecRes.payload); + expect(deletedSecretPayload).toHaveProperty("secrets"); + + // fetch secrets + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual([]); + }); } ); diff --git a/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts b/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts index 9a342d542..b97c024de 100644 --- a/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts +++ b/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts @@ -42,6 +42,7 @@ export async function up(knex: Knex): Promise { await knex.transaction(async (tx) => { const duplicateRows = await tx(TableName.OrgMembership) .select("userId", "orgId") // Select the userId and orgId so we can group by them + .whereNotNull("userId") // Ensure that the userId is not null .count("* as cnt") // Count the number of rows for each userId and orgId, so we can make sure there are more than 1 row (a duplicate) .groupBy("userId", "orgId") .havingRaw("count(*) > ?", [1]); // Using havingRaw for direct SQL expressions diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index b8ad89e45..2e66ab2ce 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -495,7 +495,11 @@ export const secretApprovalRequestServiceFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "GenSecretApproval" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d890fcc35..04b7509ca 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -282,6 +282,7 @@ export const RAW_SECRETS = { }, CREATE: { secretName: "The name of the secret to create.", + projectSlug: "The slug of the project to create the secret in.", environment: "The slug of the environment to create the secret in.", secretComment: "Attach a comment to the secret.", secretPath: "The path to create the secret in.", @@ -301,11 +302,13 @@ export const RAW_SECRETS = { }, UPDATE: { secretName: "The name of the secret to update.", + secretComment: "Update comment to the secret.", environment: "The slug of the environment where the secret is located.", secretPath: "The path of the secret to update", secretValue: "The new value of the secret.", skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to update.", + projectSlug: "The slug of the project to update the secret in.", workspaceId: "The ID of the project to update the secret in." }, DELETE: { @@ -313,6 +316,7 @@ export const RAW_SECRETS = { environment: "The slug of the environment where the secret is located.", secretPath: "The path of the secret.", type: "The type of the secret to delete.", + projectSlug: "The slug of the project to delete the secret in.", workspaceId: "The ID of the project where the secret is located." } } as const; diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index b1d852a88..955aa01be 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1656,4 +1656,263 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { return { secrets }; } }); + + server.route({ + method: "POST", + url: "/batch/raw", + config: { + rateLimit: secretsLimit + }, + schema: { + description: "Create many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectSlug: z.string().trim().describe(RAW_SECRETS.CREATE.projectSlug), + environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.CREATE.secretPath), + secrets: z + .object({ + secretKey: z.string().trim().describe(RAW_SECRETS.CREATE.secretName), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.CREATE.secretValue), + secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding) + }) + .array() + .min(1) + }), + response: { + 200: z.object({ + secrets: secretRawSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; + + const secrets = await server.services.secret.createManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectSlug, + secrets: inputSecrets + }); + + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret, i) => ({ + secretId: secret.id, + secretKey: inputSecrets[i].secretKey, + secretVersion: secret.version + })) + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretCreated, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: secrets.length, + workspaceId: secrets[0].workspace, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "PATCH", + url: "/batch/raw", + config: { + rateLimit: secretsLimit + }, + schema: { + description: "Update many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectSlug: z.string().trim().describe(RAW_SECRETS.UPDATE.projectSlug), + environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.UPDATE.secretPath), + secrets: z + .object({ + secretKey: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.UPDATE.secretValue), + secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding) + }) + .array() + .min(1) + }), + response: { + 200: z.object({ + secrets: secretRawSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; + const secrets = await server.services.secret.updateManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectSlug, + secrets: inputSecrets + }); + + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret, i) => ({ + secretId: secret.id, + secretKey: inputSecrets[i].secretKey, + secretVersion: secret.version + })) + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretUpdated, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: secrets.length, + workspaceId: secrets[0].workspace, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "DELETE", + url: "/batch/raw", + config: { + rateLimit: secretsLimit + }, + schema: { + description: "Delete many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectSlug: z.string().trim().describe(RAW_SECRETS.DELETE.projectSlug), + environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.DELETE.secretPath), + secrets: z + .object({ + secretKey: z.string().trim().describe(RAW_SECRETS.DELETE.secretName) + }) + .array() + .min(1) + }), + response: { + 200: z.object({ + secrets: secretRawSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; + const secrets = await server.services.secret.deleteManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment, + projectSlug, + secretPath, + secrets: inputSecrets + }); + + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret, i) => ({ + secretId: secret.id, + secretKey: inputSecrets[i].secretKey, + secretVersion: secret.version + })) + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretDeleted, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: secrets.length, + workspaceId: secrets[0].workspace, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 2bfda2cbe..fb2b90ba9 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -575,7 +575,11 @@ export const createManySecretsRawFnFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -680,7 +684,11 @@ export const updateManySecretsRawFnFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Update secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 3b504fbf4..584395c54 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -33,9 +33,11 @@ import { TSecretQueueFactory } from "./secret-queue"; import { TAttachSecretTagsDTO, TCreateBulkSecretDTO, + TCreateManySecretRawDTO, TCreateSecretDTO, TCreateSecretRawDTO, TDeleteBulkSecretDTO, + TDeleteManySecretRawDTO, TDeleteSecretDTO, TDeleteSecretRawDTO, TFnSecretBlindIndexCheckV2, @@ -46,6 +48,7 @@ import { TGetSecretsRawDTO, TGetSecretVersionsDTO, TUpdateBulkSecretDTO, + TUpdateManySecretRawDTO, TUpdateSecretDTO, TUpdateSecretRawDTO } from "./secret-types"; @@ -179,7 +182,11 @@ export const secretServiceFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -275,7 +282,11 @@ export const secretServiceFactory = ({ } const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -391,7 +402,11 @@ export const secretServiceFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -559,7 +574,11 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); const folderId = folder.id; const secretBlindIndex = await interalGenSecBlindIndexByName(projectId, secretName); @@ -655,7 +674,11 @@ export const secretServiceFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -724,7 +747,11 @@ export const secretServiceFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Update secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -810,7 +837,11 @@ export const secretServiceFactory = ({ await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + if (!folder) + throw new BadRequestError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -1036,6 +1067,143 @@ export const secretServiceFactory = ({ return decryptSecretRaw(secret, botKey); }; + const createManySecretsRaw = async ({ + actorId, + projectSlug, + environment, + actor, + actorOrgId, + actorAuthMethod, + secretPath, + secrets: inputSecrets = [] + }: TCreateManySecretRawDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const botKey = await projectBotService.getBotKey(projectId); + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + + const secrets = await createManySecret({ + projectId, + environment, + path: secretPath, + actor, + actorId, + actorOrgId, + actorAuthMethod, + secrets: inputSecrets.map(({ secretComment, secretKey, secretValue, skipMultilineEncoding }) => { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); + return { + secretName: secretKey, + skipMultilineEncoding, + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag + }; + }) + }); + + await snapshotService.performSnapshot(secrets[0].folderId); + await secretQueueService.syncSecrets({ secretPath, projectId, environment }); + + return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey)); + }; + + const updateManySecretsRaw = async ({ + actorId, + projectSlug, + environment, + actor, + actorOrgId, + actorAuthMethod, + secretPath, + secrets: inputSecrets = [] + }: TUpdateManySecretRawDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const botKey = await projectBotService.getBotKey(projectId); + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + + const secrets = await updateManySecret({ + projectId, + environment, + path: secretPath, + actor, + actorId, + actorOrgId, + actorAuthMethod, + secrets: inputSecrets.map(({ secretComment, secretKey, secretValue, skipMultilineEncoding }) => { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); + return { + secretName: secretKey, + type: SecretType.Shared, + skipMultilineEncoding, + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag + }; + }) + }); + + await snapshotService.performSnapshot(secrets[0].folderId); + await secretQueueService.syncSecrets({ secretPath, projectId, environment }); + + return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey)); + }; + + const deleteManySecretsRaw = async ({ + actorId, + projectSlug, + environment, + actor, + actorOrgId, + actorAuthMethod, + secretPath, + secrets: inputSecrets = [] + }: TDeleteManySecretRawDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const botKey = await projectBotService.getBotKey(projectId); + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + + const secrets = await deleteManySecret({ + projectId, + environment, + path: secretPath, + actor, + actorId, + actorOrgId, + actorAuthMethod, + secrets: inputSecrets.map(({ secretKey }) => ({ secretName: secretKey, type: SecretType.Shared })) + }); + + await snapshotService.performSnapshot(secrets[0].folderId); + await secretQueueService.syncSecrets({ secretPath, projectId, environment }); + + return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey)); + }; + const getSecretVersions = async ({ actorId, actor, @@ -1280,6 +1448,9 @@ export const secretServiceFactory = ({ createSecretRaw, updateSecretRaw, deleteSecretRaw, + createManySecretsRaw, + updateManySecretsRaw, + deleteManySecretsRaw, getSecretVersions, // external services function fnSecretBulkDelete, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 22347de4e..c2a0d5cf6 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -181,6 +181,39 @@ export type TDeleteSecretRawDTO = TProjectPermission & { type: SecretType; }; +export type TCreateManySecretRawDTO = Omit & { + secretPath: string; + projectSlug: string; + environment: string; + secrets: { + secretKey: string; + secretValue: string; + secretComment?: string; + skipMultilineEncoding?: boolean; + }[]; +}; + +export type TUpdateManySecretRawDTO = Omit & { + secretPath: string; + projectSlug: string; + environment: string; + secrets: { + secretKey: string; + secretValue: string; + secretComment?: string; + skipMultilineEncoding?: boolean; + }[]; +}; + +export type TDeleteManySecretRawDTO = Omit & { + secretPath: string; + projectSlug: string; + environment: string; + secrets: { + secretKey: string; + }[]; +}; + export type TGetSecretVersionsDTO = Omit & { limit?: number; offset?: number; diff --git a/cli/packages/cmd/folder.go b/cli/packages/cmd/folder.go index b306960f4..9cb76a312 100644 --- a/cli/packages/cmd/folder.go +++ b/cli/packages/cmd/folder.go @@ -22,10 +22,6 @@ var folderCmd = &cobra.Command{ var getCmd = &cobra.Command{ Use: "get", Short: "Get folders in a directory", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - util.RequireLocalWorkspaceFile() - util.RequireLogin() - }, Run: func(cmd *cobra.Command, args []string) { environmentName, _ := cmd.Flags().GetString("env") diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index f2f694075..305a1f0fd 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "os" "regexp" "sort" "strings" @@ -204,8 +205,10 @@ var secretsSetCmd = &cobra.Command{ // decrypt workspace key plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) + infisicalTokenEnv := os.Getenv(util.INFISICAL_TOKEN_NAME) + // pull current secrets - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath}, "") + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "") if err != nil { util.HandleError(err, "unable to retrieve secrets") } diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index 165b97534..18fe5c888 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -2,7 +2,6 @@ package util import ( "fmt" - "os" "strings" "github.com/Infisical/infisical-merge/packages/api" @@ -13,13 +12,11 @@ import ( func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder, error) { - if params.InfisicalToken == "" { - params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) - } - var foldersToReturn []models.SingleFolder var folderErr error if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" { + RequireLogin() + RequireLocalWorkspaceFile() log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details") diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index bedd77572..27f0636a9 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -307,10 +307,6 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag } func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) { - if params.InfisicalToken == "" { - params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) - } - isConnected := CheckIsConnectedToInternet() var secretsToReturn []models.SingleEnvironmentVariable // var serviceTokenDetails api.GetServiceTokenDetailsResponse diff --git a/docs/api-reference/endpoints/secrets/create-many.mdx b/docs/api-reference/endpoints/secrets/create-many.mdx new file mode 100644 index 000000000..9b0609c0a --- /dev/null +++ b/docs/api-reference/endpoints/secrets/create-many.mdx @@ -0,0 +1,8 @@ +--- +title: "Bulk Create" +openapi: "POST /api/v3/secrets/batch/raw" +--- + + + This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). + diff --git a/docs/api-reference/endpoints/secrets/delete-many.mdx b/docs/api-reference/endpoints/secrets/delete-many.mdx new file mode 100644 index 000000000..6477b2a98 --- /dev/null +++ b/docs/api-reference/endpoints/secrets/delete-many.mdx @@ -0,0 +1,8 @@ +--- +title: "Bulk Delete" +openapi: "DELETE /api/v3/secrets/batch/raw" +--- + + + This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). + diff --git a/docs/api-reference/endpoints/secrets/update-many.mdx b/docs/api-reference/endpoints/secrets/update-many.mdx new file mode 100644 index 000000000..9feaf2ca2 --- /dev/null +++ b/docs/api-reference/endpoints/secrets/update-many.mdx @@ -0,0 +1,8 @@ +--- +title: "Bulk Update" +openapi: "PATCH /api/v3/secrets/batch/raw" +--- + + + This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). + diff --git a/docs/integrations/cloud/aws-amplify.mdx b/docs/integrations/cloud/aws-amplify.mdx index 9edb5cf33..825c6c356 100644 --- a/docs/integrations/cloud/aws-amplify.mdx +++ b/docs/integrations/cloud/aws-amplify.mdx @@ -33,7 +33,7 @@ This approach enables you to fetch secrets from Infisical during Amplify build t preBuild: commands: - sudo curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.rpm.sh' | sudo -E bash - - sudo yum install infisical + - sudo yum -y install infisical ``` diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index b95a3fd4d..d127dbfb1 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -22,7 +22,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete By default, the Infisical version set in your helm chart will likely be outdated. - Choose the latest Infisical docker image tag from here [here](https://hub.docker.com/r/infisical/infisical/tags). + Choose the latest Infisical docker image tag from [here](https://hub.docker.com/r/infisical/infisical/tags). ```yaml values.yaml