From 179573a269f27ac2511431894ef802f2861b347c Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 2 Apr 2024 13:20:48 +0530 Subject: [PATCH 01/34] fix(server): added sync secret for imports and added check for avoiding cyclic import --- backend/src/queue/queue-service.ts | 4 +- .../secret-folder/secret-folder-dal.ts | 8 +-- .../secret-import/secret-import-dal.ts | 2 +- .../secret-import/secret-import-service.ts | 25 ++++++++- backend/src/services/secret/secret-queue.ts | 54 +++++++++++++++---- 5 files changed, 77 insertions(+), 16 deletions(-) diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 7cb443ae1..e1149120d 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -61,11 +61,11 @@ export type TQueueJobTypes = { }; [QueueName.SecretWebhook]: { name: QueueJobs.SecWebhook; - payload: { projectId: string; environment: string; secretPath: string }; + payload: { projectId: string; environment: string; secretPath: string; depth?: number }; }; [QueueName.IntegrationSync]: { name: QueueJobs.IntegrationSync; - payload: { projectId: string; environment: string; secretPath: string }; + payload: { projectId: string; environment: string; secretPath: string; depth?: number }; }; [QueueName.SecretFullRepoScan]: { name: QueueJobs.SecretScan; diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 023d039ca..8425b97de 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -170,7 +170,8 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str // if the given folder id is root folder id then intial path is set as / instead of /root // if not root folder the path here will be / path: db.raw(`CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))`), - child: db.raw("NULL::uuid") + child: db.raw("NULL::uuid"), + environmentSlug: `${TableName.Environment}.slug` }) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .where({ projectId }) @@ -190,14 +191,15 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str ELSE CONCAT('/', secret_folders.name) END, parent.path )` ), - child: db.raw("COALESCE(parent.child, parent.id)") + child: db.raw("COALESCE(parent.child, parent.id)"), + environmentSlug: "parent.environmentSlug" }) .from(TableName.SecretFolder) .join("parent", "parent.parentId", `${TableName.SecretFolder}.id`) ); }) .select("*") - .from("parent"); + .from("parent"); export type TSecretFolderDALFactory = ReturnType; // never change this. If u do write a migration for it diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index f9c6f1be7..a2ad4d82c 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -49,7 +49,7 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; - const find = async (filter: Partial, tx?: Knex) => { + const find = async (filter: Partial, tx?: Knex) => { try { const docs = await (tx || db)(TableName.SecretImport) .where(filter) diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 40f9797e4..ecd84e84b 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -77,10 +77,19 @@ export const secretImportServiceFactory = ({ const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" }); - // TODO(akhilmhdh-pg): updated permission check add here const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]); if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); + const sourceFolder = await folderDAL.findBySecretPath(projectId, data.environment, data.path); + if (sourceFolder) { + const existingImport = await secretImportDAL.findOne({ + folderId: sourceFolder.id, + importEnv: folder.environment.id, + importPath: path + }); + if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); + } + const secImport = await secretImportDAL.transaction(async (tx) => { const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx); return secretImportDAL.create( @@ -131,6 +140,20 @@ export const secretImportServiceFactory = ({ : await projectEnvDAL.findById(secImpDoc.importEnv); if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); + const sourceFolder = await folderDAL.findBySecretPath( + projectId, + importedEnv.slug, + data.path || secImpDoc.importPath + ); + if (sourceFolder) { + const existingImport = await secretImportDAL.findOne({ + folderId: sourceFolder.id, + importEnv: folder.environment.id, + importPath: path + }); + if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); + } + const updatedSecImport = await secretImportDAL.transaction(async (tx) => { const secImp = await secretImportDAL.findOne({ folderId: folder.id, id }); if (!secImp) throw ERR_SEC_IMP_NOT_FOUND; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index d815875df..dfeacdb0f 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -3,7 +3,7 @@ import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError } from "@app/lib/errors"; -import { isSamePath } from "@app/lib/fn"; +import { groupBy, isSamePath, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -32,7 +32,6 @@ import { interpolateSecrets } from "./secret-fns"; import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types"; export type TSecretQueueFactory = ReturnType; - type TSecretQueueFactoryDep = { queueService: TQueueServiceFactory; integrationDAL: Pick; @@ -60,6 +59,8 @@ export type TGetSecrets = { environment: string; }; +const MAX_SYNC_SECRET_DEPTH = 5; + export const secretQueueFactory = ({ queueService, integrationDAL, @@ -117,7 +118,10 @@ export const secretQueueFactory = ({ }); }; - const syncSecrets = async (dto: TGetSecrets) => { + const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => { + logger.info( + `Syncing secrets Project: ${dto.projectId} - Environment: ${dto.environment} - Path: ${dto.secretPath}` + ); await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, { jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`, removeOnFail: { count: 5 }, @@ -310,20 +314,51 @@ export const secretQueueFactory = ({ }; queueService.start(QueueName.IntegrationSync, async (job) => { - const { environment, projectId, secretPath } = job.data; + const { environment, projectId, secretPath, depth = 1 } = job.data; const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) { - logger.error("Secret path not found"); + logger.error(new Error("Secret path not found")); return; } + // start syncing all linked imports also + if (depth < MAX_SYNC_SECRET_DEPTH) { + // find all imports made with the given environment and secret path + const linkSourceDto = { + projectId, + importEnv: folder.environment.id, + importPath: secretPath + }; + const imports = await secretImportDAL.find(linkSourceDto); + if (imports.length) { + // keep calling sync secret for all the imports made + const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); + const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); + const foldersGroupedById = groupBy(importedFolders, (i) => i.id); + await Promise.all( + imports + .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path)) + .map(({ folderId }) => { + const syncDto = { + depth: depth + 1, + projectId, + secretPath: foldersGroupedById[folderId][0].path, + environment: foldersGroupedById[folderId][0].environmentSlug + }; + logger.info({ sourceLink: linkSourceDto, destination: syncDto }, `Syncing secret due to link change`); + return syncSecrets(syncDto); + }) + ); + } + } + const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); const toBeSyncedIntegrations = integrations.filter( ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) ); if (!integrations.length) return; - logger.info("Secret integration sync started", job.data, job.id); + logger.info({ source: job.data }, "Secret integration sync started - %s", job.id); for (const integration of toBeSyncedIntegrations) { const integrationAuth = { ...integration.integrationAuth, @@ -362,7 +397,7 @@ export const secretQueueFactory = ({ }); } - logger.info("Secret integration sync ended", job.id); + logger.info("Secret integration sync ended: %s", job.id); }); queueService.start(QueueName.SecretReminder, async ({ data }) => { @@ -403,7 +438,7 @@ export const secretQueueFactory = ({ }); queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => { - logger.error("Failed to sync integration", job?.data, err); + logger.error(err, "Failed to sync integration %s", job?.id); }); queueService.start(QueueName.SecretWebhook, async (job) => { @@ -411,7 +446,8 @@ export const secretQueueFactory = ({ }); return { - syncSecrets, + // depth is internal only field thus no need to make it available outside + syncSecrets: (dto: TGetSecrets) => syncSecrets(dto), syncIntegrations, addSecretReminder, removeSecretReminder, From abd62867eb78bbdb660d6af08d0aa1c9b2a3636c Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 2 Apr 2024 13:55:26 +0530 Subject: [PATCH 02/34] fix(server): resolved failing test in import --- backend/e2e-test/routes/v1/secret-import.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/e2e-test/routes/v1/secret-import.spec.ts b/backend/e2e-test/routes/v1/secret-import.spec.ts index ba37b5f42..c184e44e5 100644 --- a/backend/e2e-test/routes/v1/secret-import.spec.ts +++ b/backend/e2e-test/routes/v1/secret-import.spec.ts @@ -46,7 +46,7 @@ const deleteSecretImport = async (id: string) => { describe("Secret Import Router", async () => { test.each([ - { importEnv: "dev", importPath: "/" }, // one in root + { importEnv: "prod", importPath: "/" }, // one in root { importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones ])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => { // check for default environments @@ -66,7 +66,7 @@ describe("Secret Import Router", async () => { }); test("Get secret imports", async () => { - const createdImport1 = await createSecretImport("/", "dev"); + const createdImport1 = await createSecretImport("/", "prod"); const createdImport2 = await createSecretImport("/", "staging"); const res = await testServer.inject({ method: "GET", @@ -103,10 +103,10 @@ describe("Secret Import Router", async () => { }); test("Update secret import position", async () => { - const devImportDetails = { path: "/", envSlug: "dev" }; + const prodImportDetails = { path: "/", envSlug: "prod" }; const stagingImportDetails = { path: "/", envSlug: "staging" }; - const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug); + const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug); const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug); const updateImportRes = await testServer.inject({ @@ -136,7 +136,7 @@ describe("Secret Import Router", async () => { position: 2, importEnv: expect.objectContaining({ name: expect.any(String), - slug: expect.stringMatching(devImportDetails.envSlug), + slug: expect.stringMatching(prodImportDetails.envSlug), id: expect.any(String) }) }) @@ -166,7 +166,7 @@ describe("Secret Import Router", async () => { }); test("Delete secret import position", async () => { - const createdImport1 = await createSecretImport("/", "dev"); + const createdImport1 = await createSecretImport("/", "prod"); const createdImport2 = await createSecretImport("/", "staging"); const deletedImport = await deleteSecretImport(createdImport1.id); // check for default environments From 40bb9668fe79d256696f2fe8933b226f0eb9ef2f Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Wed, 3 Apr 2024 01:12:30 +0530 Subject: [PATCH 03/34] docs: added guide to setup integration with api --- .../overview/examples/integration.mdx | 96 +++++++++++++++++++ docs/mint.json | 3 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 docs/api-reference/overview/examples/integration.mdx diff --git a/docs/api-reference/overview/examples/integration.mdx b/docs/api-reference/overview/examples/integration.mdx new file mode 100644 index 000000000..c096826be --- /dev/null +++ b/docs/api-reference/overview/examples/integration.mdx @@ -0,0 +1,96 @@ +--- +title: "Setting Up Integration to Sync Secrets with API" +--- + +Utilizing Infisical's API, you can establish integrations to connect with external third-party providers for syncing secrets. + +While we will focus on AWS Secret Store Manager (AWS SSM) here, information for other providers can be found in their respective API reference documentation. + + +Refer to the [AWS SSM integration setup](../../../integrations/cloud/aws-secret-manager) to understand AWS SSM sync setup in UI and prerequisites. + + + + + Authentication with AWS SSM is necessary for Infisical to establish a connection. + This process is facilitated through the [Integration Auth API](../../endpoints/integrations/create-auth). + + The following are the required fields: + + This value must be **aws-secret-manager**. + + + Infisical project ID for the integration. + + + The AWS IAM User Access ID. + + + The AWS IAM User Access Secret Key. + + + Then you can send a request in the following format: + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/integration-auth/access-token \ + --header 'Authorization: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "workspaceId": "", + "integration": "aws-secret-manager", + "accessId": "", + "accessToken": "" + }' + ``` + + + + With the authentication between AWS SSM and Infisical established, you can now proceed to configure the sync behavior. + This involves defining the source (environment and secret path in Infisical) and the destination in SSM. + + This configuration is carried out through the [Integration API](../../endpoints/integrations/create). + For this, we use the [Integration API](../../endpoints/integrations/create). + + The following parameters are required: + + The ID of the integration auth object for authentication with AWS. + This will be the ID field of the previous integration auth API response. + + + Whether the integration should be active or inactive. + + + The secret name used when saving secrets in AWS SSM. This is used for naming and can be arbitrary. + + + The AWS region of the SSM. Example: `us-east-1`. + + + The Infisical environment slug from which secrets will be synced. Example: `dev`. + + + The Infisical folder path from which secrets will be synced. Example: `/some/path`. The root of the environment is `/`. + + + Then you can send a request in the following format: + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/integration \ + --header 'Authorization: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "integrationAuthId": "", + "sourceEnvironment": "", + "secretPath": "", + "app": "", + "region": "" + }' + ``` + + + + +You have successfully configured Infisical Integration to sync secrets from Infisical to AWS SSM. +[Refer the integration api reference for more information.](../../endpoints/integrations) diff --git a/docs/mint.json b/docs/mint.json index 4f70fb4c4..88cbb3d81 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -388,7 +388,8 @@ "pages": [ "api-reference/overview/examples/note", "api-reference/overview/examples/e2ee-disabled", - "api-reference/overview/examples/e2ee-enabled" + "api-reference/overview/examples/e2ee-enabled", + "api-reference/overview/examples/integration" ] } ] From f6d7ec52c2038cdbe6df3d212cfdd49dcd7d0d17 Mon Sep 17 00:00:00 2001 From: Drew Easley Date: Fri, 5 Apr 2024 08:00:47 -0400 Subject: [PATCH 04/34] fix: Run make kubectl-install --- k8-operator/README.md | 6 ++++ .../install-secrets-operator.yaml | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/k8-operator/README.md b/k8-operator/README.md index 807476c0c..f2e349f07 100644 --- a/k8-operator/README.md +++ b/k8-operator/README.md @@ -72,6 +72,12 @@ If you are editing the API definitions, generate the manifests such as CRs or CR make manifests ``` +Also, after editing the API definitions, update the kubectl-install folder: + +```sh +make kubectl-install +``` + **NOTE:** Run `make --help` for more information on all potential `make` targets More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) diff --git a/k8-operator/kubectl-install/install-secrets-operator.yaml b/k8-operator/kubectl-install/install-secrets-operator.yaml index 5a11db30a..4e66a03cd 100644 --- a/k8-operator/kubectl-install/install-secrets-operator.yaml +++ b/k8-operator/kubectl-install/install-secrets-operator.yaml @@ -96,12 +96,47 @@ spec: - secretsScope - serviceTokenSecretReference type: object + universalAuth: + properties: + credentialsRef: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + secretsScope: + properties: + envSlug: + type: string + projectSlug: + type: string + secretsPath: + type: string + required: + - envSlug + - projectSlug + - secretsPath + type: object + required: + - credentialsRef + - secretsScope + type: object type: object hostAPI: description: Infisical host to pull secrets from type: string managedSecretReference: properties: + creationPolicy: + default: Orphan + description: 'The Kubernetes Secret creation policy. Enum with values: ''Owner'', ''Orphan''. Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it. Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.' + type: string secretName: description: The name of the Kubernetes Secret type: string From a945bdfc4cd5d35f123ec3a4eabc6f5fb041b221 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 5 Apr 2024 10:07:42 -0700 Subject: [PATCH 05/34] update docs style --- docs/mint.json | 4 ++-- docs/style.css | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/mint.json b/docs/mint.json index 01c520ba0..ebd4fca0b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -472,7 +472,7 @@ ] }, { - "group": "Secret tags", + "group": "Secret Tags", "pages": [ "api-reference/endpoints/secret-tags/list", "api-reference/endpoints/secret-tags/create", @@ -492,7 +492,7 @@ ] }, { - "group": "Secret imports", + "group": "Secret Imports", "pages": [ "api-reference/endpoints/secret-imports/list", "api-reference/endpoints/secret-imports/create", diff --git a/docs/style.css b/docs/style.css index 6674ce2c6..b76d06450 100644 --- a/docs/style.css +++ b/docs/style.css @@ -63,6 +63,30 @@ border-color: #ebebeb; } +#content-area .mt-8 .rounded-xl{ + border-radius: 0; +} + +#content-area .mt-8 .rounded-lg{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-xl{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-lg{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-md{ + border-radius: 0; +} + +#content-area .mt-8 .rounded-md{ + border-radius: 0; +} + #content-area div.my-4{ border-radius: 0; border-width: 1px; @@ -78,6 +102,10 @@ border-radius: 0; } +#content-area a { + border-radius: 0; +} + #content-area .not-prose { border-radius: 0; } From b61511d1006c674da5f35ac8d02f6d4f1e79e5c4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:10:54 -0700 Subject: [PATCH 06/34] Update index.ts --- backend/src/server/routes/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f7fdb9205..d94d00ccb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -411,7 +411,12 @@ export const registerRoutes = async ( folderDAL }); - const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL }); + const projectRoleService = projectRoleServiceFactory({ + permissionService, + projectRoleDAL, + projectUserMembershipRoleDAL, + identityProjectMembershipRoleDAL + }); const snapshotService = secretSnapshotServiceFactory({ permissionService, From a16ce8899b288bb23352d19f6360250e43f8bce3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:11:15 -0700 Subject: [PATCH 07/34] Fix: Check for identities and project users who has the selected role before deleting --- .../project-role/project-role-service.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 5c8ecdcff..831af3200 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -14,16 +14,25 @@ import { import { BadRequestError } from "@app/lib/errors"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; +import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "./project-role-dal"; type TProjectRoleServiceFactoryDep = { projectRoleDAL: TProjectRoleDALFactory; permissionService: Pick; + identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; + projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; }; export type TProjectRoleServiceFactory = ReturnType; -export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: TProjectRoleServiceFactoryDep) => { +export const projectRoleServiceFactory = ({ + projectRoleDAL, + permissionService, + identityProjectMembershipRoleDAL, + projectUserMembershipRoleDAL +}: TProjectRoleServiceFactoryDep) => { const createRole = async ( actor: ActorType, actorId: string, @@ -96,8 +105,25 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); + + const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId }); + const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId }); + + if (identityRole) { + throw new BadRequestError({ + message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.", + name: "Delete role" + }); + } + if (projectUserRole) { + throw new BadRequestError({ + message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.", + name: "Delete role" + }); + } + const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId }); - if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); + if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" }); return deletedRole; }; From aca9b47f8262df213b18f57a9e3aa1dfb1bfadbf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:11:26 -0700 Subject: [PATCH 08/34] Fix: Typo --- .../MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx index e2086a6ab..b4e08f51a 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -31,7 +31,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { const [searchRoles, setSearchRoles] = useState(""); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; - + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId); @@ -49,7 +49,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { handlePopUpClose("deleteRole"); } catch (err) { console.log(err); - createNotification({ type: "error", text: "Failed to create role" }); + createNotification({ type: "error", text: "Failed to delete role" }); } }; From e26df005c2b7eebf6707462b5fbaed0e1b8b626d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:11:32 -0700 Subject: [PATCH 09/34] Fix: Typo --- .../components/ProjectRoleList/ProjectRoleList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index aa29a363b..d8a22bdb4 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -29,7 +29,7 @@ type Props = { export const ProjectRoleList = ({ onSelectRole }: Props) => { const [searchRoles, setSearchRoles] = useState(""); - + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; @@ -50,7 +50,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { handlePopUpClose("deleteRole"); } catch (err) { console.log(err); - createNotification({ type: "error", text: "Failed to create role" }); + createNotification({ type: "error", text: "Failed to delete role" }); } }; From ff2c8d017f5368e84d862696c46ccaa3210e1b81 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 5 Apr 2024 14:29:50 -0700 Subject: [PATCH 10/34] add automatic SAML redirect --- docs/self-hosting/configuration/envars.mdx | 4 ++++ .../Login/components/InitialStep/InitialStep.tsx | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index dae2e8044..db8034199 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -335,6 +335,10 @@ To login into Infisical with OAuth providers such as Google, configure the assoc Requires enterprise license. Please contact team@infisical.com to get more information. + + Configure SAML organization slug to automatically redirect all users of your Infisical instance to the identity provider. + + diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 2e2d8f79b..fb191e372 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -32,6 +32,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: const { config } = useServerConfig(); const queryParams = new URLSearchParams(window.location.search); + useEffect(() => { + if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG) { + const callbackPort = queryParams.get("callback_port"); + window.open( + `/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }` + ); + window.close(); + } + }) + const handleLogin = async (e: FormEvent) => { e.preventDefault(); try { From 74fefa9879b5dd7e48b896ed260af663bbaa9845 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 5 Apr 2024 14:39:31 -0700 Subject: [PATCH 11/34] add automatic SAML redirect --- .../src/views/Login/components/InitialStep/InitialStep.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index fb191e372..26acc97c7 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -256,7 +256,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: ) : ( -
+
)}
From 055fd34c33cbe60c364a87a985d5860d5051ee11 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 5 Apr 2024 17:25:25 -0700 Subject: [PATCH 12/34] added baked env var --- Dockerfile.standalone-infisical | 3 +++ frontend/Dockerfile | 3 +++ frontend/scripts/initialize-standalone-build.sh | 2 ++ frontend/scripts/start.sh | 2 ++ 4 files changed, 10 insertions(+) diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 1ece89639..3f90e709b 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -100,6 +100,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ ARG INTERCOM_ID=intercom-id ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \ + BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG WORKDIR / diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 2060c214a..9090fc603 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -52,6 +52,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \ + BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG ARG NEXT_INFISICAL_PLATFORM_VERSION ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh index d9138bb77..36c957a2a 100755 --- a/frontend/scripts/initialize-standalone-build.sh +++ b/frontend/scripts/initialize-standalone-build.sh @@ -4,6 +4,8 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-standalone-build-telemetry.sh true diff --git a/frontend/scripts/start.sh b/frontend/scripts/start.sh index 1db867e15..1488ad328 100644 --- a/frontend/scripts/start.sh +++ b/frontend/scripts/start.sh @@ -4,6 +4,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_P scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" +scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-telemetry.sh true From 94d509eb01ea46feadfd20da07ed6779c1e2a97f Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 5 Apr 2024 18:37:12 -0700 Subject: [PATCH 13/34] fixed search bar with folders --- frontend/src/views/SecretMainPage/SecretMainPage.tsx | 1 + .../components/FolderListView/FolderListView.tsx | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 270fd8c65..122135c03 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -313,6 +313,7 @@ export const SecretMainPage = () => { workspaceId={workspaceId} secretPath={secretPath} sortDir={sortDir} + searchTerm={filter.searchFilter} /> {canReadSecret && ( { @@ -35,8 +37,6 @@ export const FolderListView = ({ ] as const); const router = useRouter(); - - const { mutateAsync: updateFolder } = useUpdateFolder(); const { mutateAsync: deleteFolder } = useDeleteFolder(); @@ -100,6 +100,7 @@ export const FolderListView = ({ return ( <> {folders + .filter(({ name }) => name.toUpperCase().includes(String(searchTerm?.toUpperCase()))) .sort((a, b) => sortDir === SortDir.ASC ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) From fea48518a3a69bf4e83871311b29bc9e5d1f049e Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 5 Apr 2024 19:01:54 -0700 Subject: [PATCH 14/34] removed new tag from identities --- frontend/src/views/Org/MembersPage/MembersPage.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 673dc28c3..ccc868c0f 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -23,9 +23,6 @@ export const MembersPage = withPermission(

Machine Identities

-
- New -
Organization Roles From d6e5ac2133f5dc972df57075b9a273948d3b5d9f Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 6 Apr 2024 10:01:13 -0700 Subject: [PATCH 15/34] maintenance postponed --- frontend/src/pages/org/[id]/overview/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index d2b41abcd..8816193ca 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -594,6 +594,8 @@ const OrganizationPage = withPermission(
)}
+ { + window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080") &&
- Scheduled maintenance on April 6th 2024 {" "} + Scheduled maintenance on April 13th 2024 {" "}
- Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 6th, 11am EST. During these hours, read + Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 13th, 11am EST. During these hours, read operations will continue to function normally but no resources will be editable. - No action is required on your end — your applications can continue to fetch secrets. + No action is required on your end — your applications will continue to fetch secrets.
+ } +

Projects

Date: Sat, 6 Apr 2024 10:39:21 -0700 Subject: [PATCH 16/34] update april_2024_db_update_closed --- frontend/src/pages/org/[id]/overview/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 8816193ca..574d373ab 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -483,10 +483,10 @@ const OrganizationPage = withPermission( const addUsersToProject = useAddUserToWsNonE2EE(); - const { data: updateClosed } = useGetUserAction("april_2024_db_update_closed"); + const { data: updateClosed } = useGetUserAction("april_13_2024_db_update_closed"); const registerUserAction = useRegisterUserAction(); const closeUpdate = async () => { - await registerUserAction.mutateAsync("april_2024_db_update_closed"); + await registerUserAction.mutateAsync("april_13_2024_db_update_closed"); }; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ From 593bdf74b825100e6e51859876a109fdd8ff22be Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 6 Apr 2024 10:57:08 -0700 Subject: [PATCH 17/34] patch notice --- frontend/src/pages/org/[id]/overview/index.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 574d373ab..b6cc4ee46 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -594,8 +594,7 @@ const OrganizationPage = withPermission(
)}
- { - window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080") && + {window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080") && (
-
- } +
)}

Projects

From 0a52bbd55d91bd4ed536ea63192cf8abbbae2c94 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 6 Apr 2024 11:40:13 -0700 Subject: [PATCH 18/34] add render once to use effect --- frontend/src/views/Login/components/InitialStep/InitialStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 26acc97c7..2d90d68ea 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -42,7 +42,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: ); window.close(); } - }) + }, []) const handleLogin = async (e: FormEvent) => { e.preventDefault(); From 63a289c3be9522b26405fc77515987a40d5635d1 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 6 Apr 2024 11:58:50 -0700 Subject: [PATCH 19/34] add saml org clug to standalone --- Dockerfile.standalone-infisical | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 3f90e709b..2425e8588 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -35,6 +35,8 @@ ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG # Build RUN npm run build From 4e836c5dca95e014a5c83fe3ded11f35a78a5583 Mon Sep 17 00:00:00 2001 From: Juned Khan Date: Sun, 7 Apr 2024 17:41:39 +0530 Subject: [PATCH 20/34] removed extra whitespace from error message --- backend/src/services/auth/auth-login-service.ts | 2 +- backend/src/services/auth/auth-password-service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index e1b10530d..fa7439af8 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -153,7 +153,7 @@ export const authLoginServiceFactory = ({ username: email }); if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { validateProviderAuthToken(providerAuthToken as string, email); diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 42b8c2c39..4025e4903 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -192,7 +192,7 @@ export const authPaswordServiceFactory = ({ }: TCreateBackupPrivateKeyDTO) => { const userEnc = await userDAL.findUserEncKeyByUserId(userId); if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key"); @@ -239,7 +239,7 @@ export const authPaswordServiceFactory = ({ const getBackupPrivateKeyOfUser = async (userId: string) => { const user = await userDAL.findUserEncKeyByUserId(userId); if (!user || (user && !user.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId); if (!backupKey) throw new Error("Failed to find user backup key"); From 5810b76027359c77ed34d0282801d16423922034 Mon Sep 17 00:00:00 2001 From: Juned Khan Date: Mon, 8 Apr 2024 16:55:11 +0530 Subject: [PATCH 21/34] docs:fixed broken link --- docs/documentation/platform/token.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx index efb1e05d4..78445f4f1 100644 --- a/docs/documentation/platform/token.mdx +++ b/docs/documentation/platform/token.mdx @@ -44,7 +44,7 @@ In the above screenshot, you can see that we are creating a token token with `re of the `/common` path within the development environment of the project; the token expires in 6 months and can be used from any IP address. -For a deeper understanding of service tokens, it is recommended to read [this guide](http://localhost:3000/internals/service-tokens). +For a deeper understanding of service tokens, it is recommended to read [this guide](https://infisical.com/docs/internals/service-tokens). **FAQ** From 54f1a4416bf5af3f5de9ff5f459b72e7dd7383f4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 8 Apr 2024 09:06:42 -0700 Subject: [PATCH 22/34] add default value --- Dockerfile.standalone-infisical | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 2425e8588..fab4a76af 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -1,6 +1,7 @@ ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id +ARG SAML_ORG_SLUG=smal-org-slug FROM node:20-alpine AS base From 3b4bb591a3793b69234957482ffe9e44f08feea5 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 8 Apr 2024 09:25:37 -0700 Subject: [PATCH 23/34] set default for NEXT_PUBLIC_SAML_ORG_SLUG --- Dockerfile.standalone-infisical | 2 +- frontend/src/views/Login/components/InitialStep/InitialStep.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index fab4a76af..737067534 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -1,7 +1,7 @@ ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id -ARG SAML_ORG_SLUG=smal-org-slug +ARG SAML_ORG_SLUG=saml-org-slug-default FROM node:20-alpine AS base diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 2d90d68ea..abf26d691 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -33,7 +33,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: const queryParams = new URLSearchParams(window.location.search); useEffect(() => { - if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG) { + if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG && process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default") { const callbackPort = queryParams.get("callback_port"); window.open( `/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${ From 8b4d050d05a3e436ee13e6c301a178cb4bc05c0e Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 8 Apr 2024 10:15:09 -0700 Subject: [PATCH 24/34] updated original value in replace script --- frontend/scripts/initialize-standalone-build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh index 36c957a2a..859814eda 100755 --- a/frontend/scripts/initialize-standalone-build.sh +++ b/frontend/scripts/initialize-standalone-build.sh @@ -4,7 +4,7 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" -scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" From f333c905d9e711dc42453b2cd99ed40c7bfce8ff Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 8 Apr 2024 11:55:38 -0700 Subject: [PATCH 25/34] revise generic integration docs --- .../overview/examples/integration.mdx | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/docs/api-reference/overview/examples/integration.mdx b/docs/api-reference/overview/examples/integration.mdx index c096826be..e8da5ea49 100644 --- a/docs/api-reference/overview/examples/integration.mdx +++ b/docs/api-reference/overview/examples/integration.mdx @@ -1,26 +1,25 @@ --- -title: "Setting Up Integration to Sync Secrets with API" +title: "Configure native integrations programmatically" +description: "How to use Infisical API to sync secrets to external secret managers" --- -Utilizing Infisical's API, you can establish integrations to connect with external third-party providers for syncing secrets. +The Infisical API allows you to create programmatic integrations that connect with third-party secret managers to synchronize secrets from Infisical. -While we will focus on AWS Secret Store Manager (AWS SSM) here, information for other providers can be found in their respective API reference documentation. +This guide will primarily demonstrate the process using AWS Secret Store Manager (AWS SSM), but the steps are generally applicable to other secret management integrations. -Refer to the [AWS SSM integration setup](../../../integrations/cloud/aws-secret-manager) to understand AWS SSM sync setup in UI and prerequisites. + For details on setting up AWS SSM synchronization and understanding its prerequisites, refer to the [AWS SSM integration setup documentation](../../../integrations/cloud/aws-secret-manager). - - Authentication with AWS SSM is necessary for Infisical to establish a connection. - This process is facilitated through the [Integration Auth API](../../endpoints/integrations/create-auth). + + Authentication is required for all integrations. Use the [Integration Auth API](../../endpoints/integrations/create-auth) with the following parameters to authenticate. - The following are the required fields: - This value must be **aws-secret-manager**. + Set this parameter to **aws-secret-manager**. - Infisical project ID for the integration. + The Infisical project ID for the integration. The AWS IAM User Access ID. @@ -29,8 +28,6 @@ Refer to the [AWS SSM integration setup](../../../integrations/cloud/aws-secret- The AWS IAM User Access Secret Key. - Then you can send a request in the following format: - ```bash Request curl --request POST \ --url https://app.infisical.com/api/v1/integration-auth/access-token \ @@ -45,36 +42,31 @@ Refer to the [AWS SSM integration setup](../../../integrations/cloud/aws-secret- ``` - - With the authentication between AWS SSM and Infisical established, you can now proceed to configure the sync behavior. - This involves defining the source (environment and secret path in Infisical) and the destination in SSM. + + Once authentication between AWS SSM and Infisical is established, you can configure the synchronization behavior. + This involves specifying the source (environment and secret path in Infisical) and the destination in SSM to which the secrets will be synchronized. - This configuration is carried out through the [Integration API](../../endpoints/integrations/create). - For this, we use the [Integration API](../../endpoints/integrations/create). + Use the [integration API](../../endpoints/integrations/create) with the following parameters to configure the sync source and destination. - The following parameters are required: - The ID of the integration auth object for authentication with AWS. - This will be the ID field of the previous integration auth API response. + The ID of the integration authentication object used with AWS, obtained from the previous API response. - Whether the integration should be active or inactive. + Indicates whether the integration should be active or inactive. - The secret name used when saving secrets in AWS SSM. This is used for naming and can be arbitrary. + The secret name for saving in AWS SSM, which can be arbitrarily chosen. - The AWS region of the SSM. Example: `us-east-1`. + The AWS region where the SSM is located, e.g., `us-east-1`. - The Infisical environment slug from which secrets will be synced. Example: `dev`. + The Infisical environment slug from which secrets will be synchronized, e.g., `dev`. - The Infisical folder path from which secrets will be synced. Example: `/some/path`. The root of the environment is `/`. + The Infisical folder path from which secrets will be synchronized, e.g., `/some/path`. The root path is `/`. - Then you can send a request in the following format: - ```bash Request curl --request POST \ --url https://app.infisical.com/api/v1/integration \ @@ -83,7 +75,7 @@ Refer to the [AWS SSM integration setup](../../../integrations/cloud/aws-secret- --data '{ "integrationAuthId": "", "sourceEnvironment": "", - "secretPath": "", + "secretPath": "", "app": "", "region": "" }' @@ -92,5 +84,7 @@ Refer to the [AWS SSM integration setup](../../../integrations/cloud/aws-secret- -You have successfully configured Infisical Integration to sync secrets from Infisical to AWS SSM. -[Refer the integration api reference for more information.](../../endpoints/integrations) + +Congratulations! You have successfully set up an integration to synchronize secrets from Infisical with AWS SSM. +For more information, [view the integration API reference](../../endpoints/integrations). + \ No newline at end of file From 90e125454ecde79d78ea32d9bc105d4c864b5bf4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 8 Apr 2024 11:58:21 -0700 Subject: [PATCH 26/34] remove docs for e2ee --- .../overview/examples/e2ee-disabled.mdx | 180 ---- .../overview/examples/e2ee-enabled.mdx | 862 ------------------ 2 files changed, 1042 deletions(-) delete mode 100644 docs/api-reference/overview/examples/e2ee-disabled.mdx delete mode 100644 docs/api-reference/overview/examples/e2ee-enabled.mdx diff --git a/docs/api-reference/overview/examples/e2ee-disabled.mdx b/docs/api-reference/overview/examples/e2ee-disabled.mdx deleted file mode 100644 index 1a9e57552..000000000 --- a/docs/api-reference/overview/examples/e2ee-disabled.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: "E2EE Disabled" ---- - -Using Infisical's API to read/write secrets with E2EE disabled allows you to create, update, and retrieve secrets -in plaintext. Effectively, this means each such secret operation only requires 1 HTTP call. - - - - Retrieve all secrets for an Infisical project and environment. - - - ```bash - curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw?environment=environment&workspaceId=workspaceId' \ - --header 'Authorization: Bearer serviceToken' - - ``` - - - #### - - When using a [service token](../../../documentation/platform/token) with access to a single environment and path, you don't need to provide request parameters because the server will automatically scope the request to the defined environment/secrets path of the service token used. - For all other cases, request parameters are required. - - #### - - The ID of the workspace - - - The environment slug - - - Path to secrets in workspace - - - - Create a secret in Infisical. - - - - ```bash - curl --location --request POST 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretValue": "secretValue", - "secretPath": "/" - }' - ``` - - - - - Name of secret to create - - - The ID of the workspace - - - The environment slug - - - Value of secret - - - Comment of secret - - - Path to secret in workspace - - - The type of the secret. Valid options are “shared” or “personal” - - - - Retrieve a secret from Infisical. - - - - ```bash - curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \ - --header 'Authorization: Bearer serviceToken' - ``` - - - - - Name of secret to retrieve - - - The ID of the workspace - - - The environment slug - - - Path to secrets in workspace - - - The type of the secret. Valid options are “shared” or “personal” - - - - Update an existing secret in Infisical. - - - - ```bash - curl --location --request PATCH 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretValue": "secretValue", - "secretPath": "/" - }' - ``` - - - - - Name of secret to update - - - The ID of the workspace - - - The environment slug - - - Value of secret - - - Path to secret in workspace. - - - The type of the secret. Valid options are “shared” or “personal” - - - - Delete a secret in Infisical. - - - - ```bash - curl --location --request DELETE 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretPath": "/" - }' - ``` - - - - - Name of secret to update - - - The ID of the workspace - - - The environment slug - - - Path to secret in workspace. - - - The type of the secret. Valid options are “shared” or “personal” - - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/e2ee-enabled.mdx b/docs/api-reference/overview/examples/e2ee-enabled.mdx deleted file mode 100644 index 1de9c2290..000000000 --- a/docs/api-reference/overview/examples/e2ee-enabled.mdx +++ /dev/null @@ -1,862 +0,0 @@ ---- -title: "E2EE Enabled" ---- - - - E2EE enabled mode only works with [Service Tokens](/documentation/platform/token) and cannot be used with [Identities](/documentation/platform/identities/overview). - - -Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets -but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available -SDKs instead. - - - - - - Retrieve all secrets for an Infisical project and environment. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get secrets for your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecrets = data.secrets; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secrets - const secrets = encryptedSecrets.map((secret) => { - const secretKey = decrypt({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - secret: projectKey - }); - - const secretValue = decrypt({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - secret: projectKey - }); - - return ({ - secretKey, - secretValue - }); - }); - - console.log('secrets: ', secrets); -} - -getSecrets(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secrets for your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secrets = data["secrets"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secrets - secrets = [] - for secret in encrypted_secrets: - secret_key = decrypt( - ciphertext=secret["secretKeyCiphertext"], - iv=secret["secretKeyIV"], - tag=secret["secretKeyTag"], - secret=project_key, - ) - - secret_value = decrypt( - ciphertext=secret["secretValueCiphertext"], - iv=secret["secretValueIV"], - tag=secret["secretValueTag"], - secret=project_key, - ) - - secrets.append( - { - "secret_key": secret_key, - "secret_value": secret_value, - } - ) - - print("secrets:", secrets) - - -get_secrets() - -``` - - - - - - -Create a secret in Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); -const nacl = require('tweetnacl'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const createSecrets = async () => { - const serviceToken = ''; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared'; // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'some_value'; - const secretComment = 'some_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) secret to Infisical - await axios.post( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -createSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def create_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared or "personal" - secret_key = "some_key" - secret_value = "some_value" - secret_comment = "some_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) secret to Infisical - requests.post( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -create_secrets() - -``` - - - - - - - Retrieve a secret from Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecret = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get the secret from your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace, - type: secretType // optional, defaults to 'shared' - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecret = data.secret; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secret value - - const secretValue = decrypt({ - ciphertext: encryptedSecret.secretValueCiphertext, - iv: encryptedSecret.secretValueIV, - tag: encryptedSecret.secretValueTag, - secret: projectKey - }); - - console.log('secret: ', ({ - secretKey, - secretValue - })); -} - -getSecret(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secret from your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - "type": secret_type # optional, defaults to "shared" - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secret = data["secret"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secret value - secret_value = decrypt( - ciphertext=encrypted_secret["secretValueCiphertext"], - iv=encrypted_secret["secretValueIV"], - tag=encrypted_secret["secretValueTag"], - secret=project_key, - ) - - print("secret: ", { - "secret_key": secret_key, - "secret_value": secret_value - }) - - -get_secret() - -``` - - - - - - -Update an existing secret in Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const updateSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'updated_value'; - const secretComment = 'updated_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your updated secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) updated secret to Infisical - await axios.patch( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -updateSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def update_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - secret_value = "updated_value" - secret_comment = "updated_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your updated secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) updated secret to Infisical - requests.patch( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -update_secret() - -``` - - - - - - - Delete a secret in Infisical. -```js -const axios = require('axios'); -const BASE_URL = 'https://app.infisical.com'; - -const deleteSecrets = async () => { - const serviceToken = 'your_service_token'; - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key' - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Delete secret from Infisical - await axios.delete( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - }, - } - ); -}; - -deleteSecrets(); -``` - - - -```Python -import requests - -BASE_URL = "https://app.infisical.com" - - -def delete_secrets(): - service_token = "" - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Delete secret from Infisical - requests.delete( - f"{BASE_URL}/api/v2/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -delete_secrets() - -``` - - - - If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header. - - - - \ No newline at end of file From f6cc20b08bcf7f9d3b6eba35a2b209363fdeece6 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 8 Apr 2024 12:00:11 -0700 Subject: [PATCH 27/34] remove link from docs --- docs/mint.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/mint.json b/docs/mint.json index 4b4036585..5f3836e49 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -388,8 +388,6 @@ "group": "Examples", "pages": [ "api-reference/overview/examples/note", - "api-reference/overview/examples/e2ee-disabled", - "api-reference/overview/examples/e2ee-enabled", "api-reference/overview/examples/integration" ] } From 4774469244d8c8025451d73cc5585871436f08ee Mon Sep 17 00:00:00 2001 From: Juned Khan Date: Tue, 9 Apr 2024 14:05:45 +0530 Subject: [PATCH 28/34] docs: fixed another broken link --- docs/documentation/getting-started/introduction.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index bd9354aa1..0f414c62a 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -38,7 +38,7 @@ Infisical helps developers achieve secure centralized secret management and prov - Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. - Secure and compliant secret management practices in **[production environments](/sdks/overview)**. - **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more. -- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](http://localhost:3000/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. +- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. ## How does Infisical work? From 7116c85f2cb1f339ad2b9b5f5c856630da8e3455 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 9 Apr 2024 20:51:19 -0400 Subject: [PATCH 29/34] remove note --- .../overview/examples/integration.mdx | 2 +- docs/api-reference/overview/examples/note.mdx | 54 ------------------- docs/mint.json | 1 - 3 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 docs/api-reference/overview/examples/note.mdx diff --git a/docs/api-reference/overview/examples/integration.mdx b/docs/api-reference/overview/examples/integration.mdx index e8da5ea49..71f5b6de4 100644 --- a/docs/api-reference/overview/examples/integration.mdx +++ b/docs/api-reference/overview/examples/integration.mdx @@ -1,5 +1,5 @@ --- -title: "Configure native integrations programmatically" +title: "Configure native integrations via API" description: "How to use Infisical API to sync secrets to external secret managers" --- diff --git a/docs/api-reference/overview/examples/note.mdx b/docs/api-reference/overview/examples/note.mdx deleted file mode 100644 index 8491dfaae..000000000 --- a/docs/api-reference/overview/examples/note.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "Note on E2EE" ---- - -Each project in Infisical can have **End-to-End Encryption (E2EE)** enabled or disabled. - -By default, all projects have **E2EE** enabled which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side; this can be (optionally) disabled. However, this has limitations around functionality and ease-of-use: - -- You cannot make HTTP calls to Infisical to read/write secrets in plaintext. -- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation. - - - - Example read/write secrets without client-side encryption/decryption - - - Example read/write secrets with client-side encryption/decryption - - - -## FAQ - - - - We recommend starting with having **E2EE** enabled and disabling it if: - - - You're self-hosting Infisical, so having your instance of Infisical be able to read your secrets isn't an issue. - - You want an easier way to read/write secrets with Infisical. - - You need more power out of non-E2EE features such as secret rotation, dynamic secrets, etc. - - - - You can enable/disable E2EE for your project in Infisical in the Project Settings. - - - It is secure and in fact how most vendors in our industry are able to offer features like secret rotation. In this mode, secrets are encrypted at rest by - a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server. - - If you're concerned about Infisical Cloud's ability to read your secrets, then you may wish to - use it with **E2EE** enabled or self-host Infisical on your own infrastructure and disable E2EE there. - - As an organization, we do not read any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization. - - \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 5f3836e49..4ab246f83 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -387,7 +387,6 @@ { "group": "Examples", "pages": [ - "api-reference/overview/examples/note", "api-reference/overview/examples/integration" ] } From 85489a81ff85c4024b414aa0becbbdd18c0cb736 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 10 Apr 2024 21:18:26 -0700 Subject: [PATCH 30/34] Add resync on integration import creation/deletion and update forward/backward recursive logic for syncing dependent imports --- backend/src/server/routes/index.ts | 17 +-- .../secret-folder/secret-folder-dal.ts | 2 + .../secret-import/secret-import-service.ts | 18 ++- backend/src/services/secret/secret-queue.ts | 107 ++++++++++-------- 4 files changed, 85 insertions(+), 59 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index d94d00ccb..be0494f0e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -445,14 +445,6 @@ export const registerRoutes = async ( projectEnvDAL, snapshotService }); - const secretImportService = secretImportServiceFactory({ - projectEnvDAL, - folderDAL, - permissionService, - secretImportDAL, - projectDAL, - secretDAL - }); const integrationAuthService = integrationAuthServiceFactory({ integrationAuthDAL, integrationDAL, @@ -480,6 +472,15 @@ export const registerRoutes = async ( secretTagDAL, secretVersionTagDAL }); + const secretImportService = secretImportServiceFactory({ + projectEnvDAL, + folderDAL, + permissionService, + secretImportDAL, + projectDAL, + secretDAL, + secretQueueService + }); const secretBlindIndexService = secretBlindIndexServiceFactory({ permissionService, secretDAL, diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 8425b97de..b3147d1fa 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -259,10 +259,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => { try { const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds); + const rootFolders = groupBy( folders.filter(({ parentId }) => parentId === null), (i) => i.child || i.id // root condition then child and parent will null ); + return folderIds.map((folderId) => rootFolders[folderId]?.[0]); } catch (error) { throw new DatabaseError({ error, name: "Find by secret path" }); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index ecd84e84b..2d59284d4 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -7,6 +7,7 @@ import { BadRequestError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; +import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; import { fnSecretsFromImports } from "./secret-import-fns"; @@ -25,6 +26,7 @@ type TSecretImportServiceFactoryDep = { projectDAL: Pick; projectEnvDAL: TProjectEnvDALFactory; permissionService: Pick; + secretQueueService: Pick; }; const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" }); @@ -37,7 +39,8 @@ export const secretImportServiceFactory = ({ permissionService, folderDAL, projectDAL, - secretDAL + secretDAL, + secretQueueService }: TSecretImportServiceFactoryDep) => { const createImport = async ({ environment, @@ -103,6 +106,12 @@ export const secretImportServiceFactory = ({ ); }); + await secretQueueService.syncSecrets({ + secretPath: secImport.importPath, + projectId, + environment: importEnv.slug + }); + return { ...secImport, importEnv }; }; @@ -208,6 +217,13 @@ export const secretImportServiceFactory = ({ if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); return { ...doc, importEnv }; }); + + await secretQueueService.syncSecrets({ + secretPath: path, + projectId, + environment + }); + return secImport; }; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index dfeacdb0f..1d2561723 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -23,7 +23,6 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; -import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { fnTriggerWebhook } from "../webhook/webhook-fns"; @@ -231,62 +230,35 @@ export const secretQueueFactory = ({ } }; - const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => { + type Content = Record; + + /** + * Return the secrets in a given [folderId] including the secrets from nested imported folders + */ + const getIntegrationSecrets = async (dto: { + projectId: string; + environment: string; + folderId: string; + key: string; + depth: number; + }) => { + let content: Content = {}; + if (dto.depth > MAX_SYNC_SECRET_DEPTH) return content; + const secrets = await secretDAL.findByFolderId(dto.folderId); - - // get imported secrets - const secretImport = await secretImportDAL.find({ folderId: dto.folderId }); - const importedSecrets = await fnSecretsFromImports({ - allowedImports: secretImport, - secretDAL, - folderDAL - }); - - if (!secrets.length && !importedSecrets.length) return {}; - - const content: Record = {}; - - importedSecrets.forEach(({ secrets: secs }) => { - secs.forEach((secret) => { - const secretKey = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key - }); - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); - content[secretKey] = { value: secretValue }; - content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); - - if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { - const commentValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretCommentCiphertext, - iv: secret.secretCommentIV, - tag: secret.secretCommentTag, - key - }); - content[secretKey].comment = commentValue; - } - }); - }); secrets.forEach((secret) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, - key + key: dto.key }); const secretValue = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretValueCiphertext, iv: secret.secretValueIV, tag: secret.secretValueTag, - key + key: dto.key }); content[secretKey] = { value: secretValue }; @@ -296,25 +268,52 @@ export const secretQueueFactory = ({ ciphertext: secret.secretCommentCiphertext, iv: secret.secretCommentIV, tag: secret.secretCommentTag, - key + key: dto.key }); content[secretKey].comment = commentValue; } content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); }); + const expandSecrets = interpolateSecrets({ projectId: dto.projectId, - secretEncKey: key, + secretEncKey: dto.key, folderDAL, secretDAL }); + await expandSecrets(content); + + const secretImport = await secretImportDAL.find({ folderId: dto.folderId }); + if (!secretImport) return content; + + const importedFolders = await folderDAL.findByManySecretPath( + secretImport.map(({ importEnv, importPath }) => ({ + envId: importEnv.id, + secretPath: importPath + })) + ); + + for await (const folder of importedFolders) { + if (folder) { + const importedSecrets = await getIntegrationSecrets({ + environment: dto.environment, + projectId: dto.projectId, + folderId: folder.id, + key: dto.key, + depth: dto.depth + 1 + }); + content = { ...content, ...importedSecrets }; + } + } + return content; }; queueService.start(QueueName.IntegrationSync, async (job) => { const { environment, projectId, secretPath, depth = 1 } = job.data; + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) { logger.error(new Error("Secret path not found")); @@ -330,11 +329,12 @@ export const secretQueueFactory = ({ importPath: secretPath }; const imports = await secretImportDAL.find(linkSourceDto); + if (imports.length) { // keep calling sync secret for all the imports made const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); - const foldersGroupedById = groupBy(importedFolders, (i) => i.id); + const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id); await Promise.all( imports .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path)) @@ -352,8 +352,9 @@ export const secretQueueFactory = ({ } } - const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); + const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment const toBeSyncedIntegrations = integrations.filter( + // note: sync only the integrations sourced from secretPath ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) ); @@ -369,7 +370,13 @@ export const secretQueueFactory = ({ const botKey = await projectBotService.getBotKey(projectId); const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey); - const secrets = await getIntegrationSecrets({ environment, projectId, secretPath, folderId: folder.id }, botKey); + const secrets = await getIntegrationSecrets({ + environment, + projectId, + folderId: folder.id, + key: botKey, + depth: 1 + }); const suffixedSecrets: typeof secrets = {}; const metadata = integration.metadata as Record; if (metadata) { From 2ac785493a321f74ec7fc70a27bfcf6edf419811 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 10 Apr 2024 21:52:33 -0700 Subject: [PATCH 31/34] Add comments to explain new getIntegrationSecrets --- backend/src/services/secret/secret-queue.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 1d2561723..ddef9269d 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -233,7 +233,8 @@ export const secretQueueFactory = ({ type Content = Record; /** - * Return the secrets in a given [folderId] including the secrets from nested imported folders + * Return the secrets in a given [folderId] including secrets from + * nested imported folders recursively. */ const getIntegrationSecrets = async (dto: { projectId: string; @@ -245,6 +246,7 @@ export const secretQueueFactory = ({ let content: Content = {}; if (dto.depth > MAX_SYNC_SECRET_DEPTH) return content; + // process secrets in current folder const secrets = await secretDAL.findByFolderId(dto.folderId); secrets.forEach((secret) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ @@ -285,7 +287,10 @@ export const secretQueueFactory = ({ await expandSecrets(content); + // check if current folder has any imports from other folders const secretImport = await secretImportDAL.find({ folderId: dto.folderId }); + + // if no imports then return secrets in the current folder if (!secretImport) return content; const importedFolders = await folderDAL.findByManySecretPath( @@ -297,6 +302,8 @@ export const secretQueueFactory = ({ for await (const folder of importedFolders) { if (folder) { + // get secrets contained in each imported folder by recursively calling + // this function against the imported folder const importedSecrets = await getIntegrationSecrets({ environment: dto.environment, projectId: dto.projectId, @@ -304,6 +311,8 @@ export const secretQueueFactory = ({ key: dto.key, depth: dto.depth + 1 }); + + // add the imported secrets to the current folder secrets content = { ...content, ...importedSecrets }; } } From a01a995585131c97c32aaf0a0fe78546040adfbf Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 10 Apr 2024 21:52:33 -0700 Subject: [PATCH 32/34] Add comments to explain new getIntegrationSecrets --- backend/src/services/secret/secret-queue.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index ddef9269d..c96088e7b 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -119,7 +119,7 @@ export const secretQueueFactory = ({ const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => { logger.info( - `Syncing secrets Project: ${dto.projectId} - Environment: ${dto.environment} - Path: ${dto.secretPath}` + `syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]` ); await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, { jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`, @@ -244,7 +244,12 @@ export const secretQueueFactory = ({ depth: number; }) => { let content: Content = {}; - if (dto.depth > MAX_SYNC_SECRET_DEPTH) return content; + if (dto.depth > MAX_SYNC_SECRET_DEPTH) { + logger.info( + `getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]` + ); + return content; + } // process secrets in current folder const secrets = await secretDAL.findByFolderId(dto.folderId); @@ -354,11 +359,17 @@ export const secretQueueFactory = ({ secretPath: foldersGroupedById[folderId][0].path, environment: foldersGroupedById[folderId][0].environmentSlug }; - logger.info({ sourceLink: linkSourceDto, destination: syncDto }, `Syncing secret due to link change`); + logger.info( + `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` + ); return syncSecrets(syncDto); }) ); } + } else { + logger.info( + `getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}] [depth=${depth}]` + ); } const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment @@ -368,7 +379,9 @@ export const secretQueueFactory = ({ ); if (!integrations.length) return; - logger.info({ source: job.data }, "Secret integration sync started - %s", job.id); + logger.info( + `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` + ); for (const integration of toBeSyncedIntegrations) { const integrationAuth = { ...integration.integrationAuth, From 0db682c5f05ae9a0d8a2c69a2a684219044a95eb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 11 Apr 2024 10:10:42 -0400 Subject: [PATCH 33/34] remove depth from exceed message --- backend/src/services/secret/secret-queue.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index c96088e7b..1fc6b1109 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -360,16 +360,14 @@ export const secretQueueFactory = ({ environment: foldersGroupedById[folderId][0].environmentSlug }; logger.info( - `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` + `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` ); return syncSecrets(syncDto); }) ); } } else { - logger.info( - `getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}] [depth=${depth}]` - ); + logger.info(`getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}]`); } const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment From ee0c79d018a19d6e9ffe0747f631d9fc3ff39db6 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 11 Apr 2024 11:25:28 -0700 Subject: [PATCH 34/34] Delete integration auth upon integration deletion if no other integrations share the same auth --- .../integration/integration-service.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 5bf412c9b..a3c4c84db 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -146,7 +146,27 @@ export const integrationServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); - const deletedIntegration = await integrationDAL.deleteById(id); + const deletedIntegration = await integrationDAL.transaction(async (tx) => { + // delete integration + const deletedIntegrationResult = await integrationDAL.deleteById(id, tx); + + // check if there are other integrations that share the same integration auth + const integrations = await integrationDAL.find( + { + integrationAuthId: integration.integrationAuthId + }, + tx + ); + + if (integrations.length === 0) { + // no other integration shares the same integration auth + // -> delete the integration auth + await integrationAuthDAL.deleteById(integration.integrationAuthId, tx); + } + + return deletedIntegrationResult; + }); + return { ...integration, ...deletedIntegration }; };