From 6cab7504fc8e1b165347989040984ec531d0a6f0 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 15 Aug 2024 22:04:32 +0530 Subject: [PATCH 1/6] fix: switched sync integration to have redis lock --- backend/src/keystore/keystore.ts | 35 ++- backend/src/lib/fn/dates.ts | 6 + backend/src/server/routes/index.ts | 1 + backend/src/services/secret/secret-queue.ts | 248 ++++++++++++-------- 4 files changed, 176 insertions(+), 114 deletions(-) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index d7bbb0c79..ab5920204 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -5,17 +5,26 @@ import { Redlock, Settings } from "@app/lib/red-lock"; export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict -export enum KeyStorePrefixes { - SecretReplication = "secret-replication-import-lock", - KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock", - KmsProjectKeyCreation = "kms-project-key-creation-lock", - WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-", - WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-", - KmsOrgKeyCreation = "kms-org-key-creation-lock", - KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock", - WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-", - WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-" -} +export const KeyStorePrefixes = { + SecretReplication: "secret-replication-import-lock", + KmsProjectDataKeyCreation: "kms-project-data-key-creation-lock", + KmsProjectKeyCreation: "kms-project-key-creation-lock", + WaitUntilReadyKmsProjectDataKeyCreation: "wait-until-ready-kms-project-data-key-creation-", + WaitUntilReadyKmsProjectKeyCreation: "wait-until-ready-kms-project-key-creation-", + KmsOrgKeyCreation: "kms-org-key-creation-lock", + KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock", + WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-", + WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-", + + SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) => + `sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const, + SetSyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => + `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const +}; + +export const KeyStoreTtls = { + SetSyncSecretIntegrationLastRunTimestampInSeconds: 10 +}; type TWaitTillReady = { key: string; @@ -37,10 +46,10 @@ export const keyStoreFactory = (redisUrl: string) => { const setItemWithExpiry = async ( key: string, - exp: number | string, + expiryInSeconds: number | string, value: string | number | Buffer, prefix?: string - ) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", exp); + ) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds); const deleteItem = async (key: string) => redis.del(key); diff --git a/backend/src/lib/fn/dates.ts b/backend/src/lib/fn/dates.ts index f9ea4db10..cd5ca5c12 100644 --- a/backend/src/lib/fn/dates.ts +++ b/backend/src/lib/fn/dates.ts @@ -1,2 +1,8 @@ export const getLastMidnightDateISO = (last = 1) => `${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`; + +export const getTimeDifferenceInSeconds = (lhsTimestamp: string, rhsTimestamp: string) => { + const lhs = new Date(lhsTimestamp); + const rhs = new Date(rhsTimestamp); + return Math.floor((Number(lhs) - Number(rhs)) / 1000); +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 5116261d5..910e81507 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -711,6 +711,7 @@ export const registerRoutes = async ( kmsService }); const secretQueueService = secretQueueFactory({ + keyStore, queueService, secretDAL, folderDAL, diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 84e49aabe..c572e5194 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -6,11 +6,12 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal"; +import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore"; 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 { groupBy, isSamePath, unique } from "@app/lib/fn"; +import { getTimeDifferenceInSeconds, 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"; @@ -79,6 +80,7 @@ type TSecretQueueFactoryDep = { secretApprovalRequestDAL: Pick; snapshotDAL: Pick; snapshotSecretV2BridgeDAL: Pick; + keyStore: Pick; }; export type TGetSecrets = { @@ -122,7 +124,8 @@ export const secretQueueFactory = ({ secretRotationDAL, snapshotDAL, snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL + secretApprovalRequestDAL, + keyStore }: TSecretQueueFactoryDep) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const appCfg = getConfig(); @@ -576,7 +579,6 @@ export const secretQueueFactory = ({ ) ); } - const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, @@ -641,111 +643,155 @@ export const secretQueueFactory = ({ `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}]` ); - const secrets = shouldUseSecretV2Bridge - ? await getIntegrationSecretsV2({ - environment, - projectId, - folderId: folder.id, - depth: 1, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") - }) - : await getIntegrationSecrets({ - environment, - projectId, - folderId: folder.id, - key: botKey as string, - depth: 1 - }); - - for (const integration of toBeSyncedIntegrations) { - const integrationAuth = { - ...integration.integrationAuth, - createdAt: new Date(), - updatedAt: new Date(), - projectId: integration.projectId - }; - - const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( - integrationAuth, - shouldUseSecretV2Bridge, - botKey - ); - let awsAssumeRoleArn = null; - if (shouldUseSecretV2Bridge) { - if (integrationAuth.encryptedAwsAssumeIamRoleArn) { - awsAssumeRoleArn = secretManagerDecryptor({ - cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) - }).toString(); - } - } else if ( - integrationAuth.awsAssumeIamRoleArnTag && - integrationAuth.awsAssumeIamRoleArnIV && - integrationAuth.awsAssumeIamRoleArnCipherText - ) { - awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, - iv: integrationAuth.awsAssumeIamRoleArnIV, - tag: integrationAuth.awsAssumeIamRoleArnTag, - key: botKey as string - }); + const lock = await keyStore.acquireLock( + [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)], + 10000, + { + retryCount: 3, + retryDelay: 2000 } + ); + const lockAcquiredTime = new Date(); - const suffixedSecrets: typeof secrets = {}; - const metadata = integration.metadata as Record; - if (metadata) { - Object.keys(secrets).forEach((key) => { - const prefix = metadata?.secretPrefix || ""; - const suffix = metadata?.secretSuffix || ""; - const newKey = prefix + key + suffix; - suffixedSecrets[newKey] = secrets[key]; - }); - } + const lastRunSyncIntegrationTimestamp = await keyStore.getItem( + KeyStorePrefixes.SetSyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) + ); - try { - // akhilmhdh: this needs to changed later to be more easier to use - // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places - const response = await syncIntegrationSecrets({ - createManySecretsRawFn, - updateManySecretsRawFn, - integrationDAL, - integration, - integrationAuth, - secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, - accessId: accessId as string, - awsAssumeRoleArn, - accessToken, - projectId, - appendices: { - prefix: metadata?.secretPrefix || "", - suffix: metadata?.secretSuffix || "" - } - }); - - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - lastUsed: new Date(), - syncMessage: response?.syncMessage ?? "", - isSynced: response?.isSynced ?? true - }); - } catch (err) { - logger.error( - err, - `Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]` + if (lastRunSyncIntegrationTimestamp) { + const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); + if (isStaleSyncIntegration) { + logger.info( + `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` ); - - const message = - (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || - "Unknown error occurred."; - - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - lastUsed: new Date(), - syncMessage: message, - isSynced: false - }); + return; } + + const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds( + lockAcquiredTime.toISOString(), + lastRunSyncIntegrationTimestamp + ); + if (timeDifferenceWithLastIntegration < 2000 && timeDifferenceWithLastIntegration > 0) + await new Promise((resolve) => { + setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000); + }); } + // akhilmhdh: this try catch is for lock release + try { + const secrets = shouldUseSecretV2Bridge + ? await getIntegrationSecretsV2({ + environment, + projectId, + folderId: folder.id, + depth: 1, + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + }) + : await getIntegrationSecrets({ + environment, + projectId, + folderId: folder.id, + key: botKey as string, + depth: 1 + }); + + for (const integration of toBeSyncedIntegrations) { + const integrationAuth = { + ...integration.integrationAuth, + createdAt: new Date(), + updatedAt: new Date(), + projectId: integration.projectId + }; + + const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( + integrationAuth, + shouldUseSecretV2Bridge, + botKey + ); + let awsAssumeRoleArn = null; + if (shouldUseSecretV2Bridge) { + if (integrationAuth.encryptedAwsAssumeIamRoleArn) { + awsAssumeRoleArn = secretManagerDecryptor({ + cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) + }).toString(); + } + } else if ( + integrationAuth.awsAssumeIamRoleArnTag && + integrationAuth.awsAssumeIamRoleArnIV && + integrationAuth.awsAssumeIamRoleArnCipherText + ) { + awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, + iv: integrationAuth.awsAssumeIamRoleArnIV, + tag: integrationAuth.awsAssumeIamRoleArnTag, + key: botKey as string + }); + } + + const suffixedSecrets: typeof secrets = {}; + const metadata = integration.metadata as Record; + if (metadata) { + Object.keys(secrets).forEach((key) => { + const prefix = metadata?.secretPrefix || ""; + const suffix = metadata?.secretSuffix || ""; + const newKey = prefix + key + suffix; + suffixedSecrets[newKey] = secrets[key]; + }); + } + + // akhilmhdh: this try catch is for catching integration error and saving it in db + try { + // akhilmhdh: this needs to changed later to be more easier to use + // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places + const response = await syncIntegrationSecrets({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, + integration, + integrationAuth, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, + accessId: accessId as string, + awsAssumeRoleArn, + accessToken, + projectId, + appendices: { + prefix: metadata?.secretPrefix || "", + suffix: metadata?.secretSuffix || "" + } + }); + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: response?.syncMessage ?? "", + isSynced: response?.isSynced ?? true + }); + } catch (err) { + logger.error( + err, + `Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]` + ); + + const message = + (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || + "Unknown error occurred."; + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: message, + isSynced: false + }); + } + } + } finally { + await lock.release(); + } + + await keyStore.setItemWithExpiry( + KeyStorePrefixes.SetSyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), + KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, + lockAcquiredTime.toISOString() + ); logger.info("Secret integration sync ended: %s", job.id); }); From cc94a3366a1935ec5e6c23db83259d13dd4c54d6 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 15 Aug 2024 23:38:16 +0530 Subject: [PATCH 2/6] feat: made requested changes for integration sync --- backend/src/keystore/keystore.ts | 2 +- backend/src/services/secret/secret-queue.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index ab5920204..f6d5d1c6e 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -18,7 +18,7 @@ export const KeyStorePrefixes = { SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) => `sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const, - SetSyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => + SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const }; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index c572e5194..36edcf195 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -654,10 +654,12 @@ export const secretQueueFactory = ({ const lockAcquiredTime = new Date(); const lastRunSyncIntegrationTimestamp = await keyStore.getItem( - KeyStorePrefixes.SetSyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) ); + // check whether the integration should wait or not if (lastRunSyncIntegrationTimestamp) { + const INTEGRATION_INTERVAL = 2000; const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); if (isStaleSyncIntegration) { logger.info( @@ -670,7 +672,7 @@ export const secretQueueFactory = ({ lockAcquiredTime.toISOString(), lastRunSyncIntegrationTimestamp ); - if (timeDifferenceWithLastIntegration < 2000 && timeDifferenceWithLastIntegration > 0) + if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0) await new Promise((resolve) => { setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000); }); @@ -788,7 +790,7 @@ export const secretQueueFactory = ({ } await keyStore.setItemWithExpiry( - KeyStorePrefixes.SetSyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, lockAcquiredTime.toISOString() ); From 27d2af497939e4c321152c559409685a81d3ce0b Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 15 Aug 2024 22:39:49 -0400 Subject: [PATCH 3/6] Add DISABLE_AUDIT_LOG_GENERATION Added `DISABLE_AUDIT_LOG_GENERATION` which when set to true will prevent the creation of audit logs in Infisical. This will be used for load testing purposes and help verify if audit logs are a bottle neck for performance --- backend/src/ee/services/audit-log/audit-log-service.ts | 5 +++++ backend/src/lib/config/env.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index 1564c6dcb..2916eb412 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { TPermissionServiceFactory } from "../permission/permission-service"; @@ -61,6 +62,10 @@ export const auditLogServiceFactory = ({ }; const createAuditLog = async (data: TCreateAuditLogDTO) => { + const appCfg = getConfig(); + if (appCfg.DISABLE_AUDIT_LOG_GENERATION) { + return; + } // add all cases in which project id or org id cannot be added if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" }); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 90f04d952..8a2b961e9 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -140,7 +140,8 @@ const envSchema = z MAINTENANCE_MODE: zodStrBool.default("false"), CAPTCHA_SECRET: zpStr(z.string().optional()), PLAIN_API_KEY: zpStr(z.string().optional()), - PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()) + PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()), + DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false") }) .transform((data) => ({ ...data, From 32046ca880e0b26ff67edfcee0aaf1be6c0413c4 Mon Sep 17 00:00:00 2001 From: DC Date: Thu, 15 Aug 2024 20:17:40 -0700 Subject: [PATCH 4/6] fix: added missing word --- docs/documentation/guides/local-development.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/documentation/guides/local-development.mdx b/docs/documentation/guides/local-development.mdx index 9ffc0fca0..6d606bafe 100644 --- a/docs/documentation/guides/local-development.mdx +++ b/docs/documentation/guides/local-development.mdx @@ -9,7 +9,7 @@ description: "Learn how to manage secrets in local development environments." There is a number of issues that arise with secret management in local development environment: 1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another. 2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time. -3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability. +3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded secret as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability. ## Solution @@ -31,4 +31,4 @@ By default, all the secrets in the Infisical environments are shared among proje ### Secret Scanning -In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub. \ No newline at end of file +In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub. From 34c220026961845cabbeff3028161d9ee129839f Mon Sep 17 00:00:00 2001 From: = Date: Fri, 16 Aug 2024 16:20:38 +0530 Subject: [PATCH 5/6] fix: resolved add all members to project failing when there is pending users --- frontend/src/layouts/AppLayout/AppLayout.tsx | 8 +++++--- frontend/src/pages/org/[id]/overview/index.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 6a65e32e8..4e3602686 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -273,8 +273,10 @@ export const AppLayout = ({ children }: LayoutProps) => { const orgUsers = await fetchOrgUsers(currentOrg.id); await addUsersToProject.mutateAsync({ usernames: orgUsers - .map((member) => member.user.username) - .filter((username) => username !== user.username), + .filter( + (member) => member.user.username !== user.username && member.status === "accepted" + ) + .map((member) => member.user.username), projectId: newProjectId, orgId: currentOrg.id }); @@ -482,7 +484,7 @@ export const AppLayout = ({ children }: LayoutProps) => { )} - Organization Admin Console + Organization Admin Console
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index babf1a15f..c93649f2c 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -552,8 +552,10 @@ const OrganizationPage = () => { await addUsersToProject.mutateAsync({ usernames: orgUsers - .map((member) => member.user.username) - .filter((username) => username !== user.username), + .filter( + (member) => member.user.username !== user.username && member.status === "accepted" + ) + .map((member) => member.user.username), projectId: newProjectId, orgId: currentOrg.id }); From a00103aa1e07097c80951ceac0251633114d0682 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 16 Aug 2024 22:34:10 +0530 Subject: [PATCH 6/6] feat: license check in secret approval api level --- .../secret-approval-policy-service.ts | 30 ++++++++++++++++++- .../secret-approval-request-service.ts | 29 +++++++++++++++++- backend/src/server/routes/index.ts | 6 ++-- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 366ad69ea..8c974b05d 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@app/lib/fn"; import { containsGlobPatterns } from "@app/lib/picomatch"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TLicenseServiceFactory } from "../license/license-service"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { @@ -28,6 +29,7 @@ type TSecretApprovalPolicyServiceFactoryDep = { secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; projectEnvDAL: Pick; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; + licenseService: Pick; }; export type TSecretApprovalPolicyServiceFactory = ReturnType; @@ -36,7 +38,8 @@ export const secretApprovalPolicyServiceFactory = ({ secretApprovalPolicyDAL, permissionService, secretApprovalPolicyApproverDAL, - projectEnvDAL + projectEnvDAL, + licenseService }: TSecretApprovalPolicyServiceFactoryDep) => { const createSecretApprovalPolicy = async ({ name, @@ -65,6 +68,15 @@ export const secretApprovalPolicyServiceFactory = ({ ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval ); + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to create secret approval policy due to plan restriction. Upgrade plan to create secret approval policy." + }); + } + const env = await projectEnvDAL.findOne({ slug: environment, projectId }); if (!env) throw new BadRequestError({ message: "Environment not found" }); @@ -115,6 +127,14 @@ export const secretApprovalPolicyServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy." + }); + } + const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => { const doc = await secretApprovalPolicyDAL.updateById( secretApprovalPolicy.id, @@ -167,6 +187,14 @@ export const secretApprovalPolicyServiceFactory = ({ ProjectPermissionSub.SecretApproval ); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy." + }); + } + await secretApprovalPolicyDAL.deleteById(secretPolicyId); return sapPolicy; }; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 81ccf35f6..f6bb33168 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -50,6 +50,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; +import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; @@ -97,6 +98,7 @@ type TSecretApprovalRequestServiceFactoryDep = { >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; + licenseService: Pick; }; export type TSecretApprovalRequestServiceFactory = ReturnType; @@ -122,7 +124,8 @@ export const secretApprovalRequestServiceFactory = ({ kmsService, secretV2BridgeDAL, secretVersionV2BridgeDAL, - secretVersionTagV2BridgeDAL + secretVersionTagV2BridgeDAL, + licenseService }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); @@ -295,6 +298,14 @@ export const secretApprovalRequestServiceFactory = ({ if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to review secret approval request due to plan restriction. Upgrade plan to review secret approval request." + }); + } + const { policy } = secretApprovalRequest; const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, @@ -345,6 +356,14 @@ export const secretApprovalRequestServiceFactory = ({ if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to update secret approval request due to plan restriction. Upgrade plan to update secret approval request." + }); + } + const { policy } = secretApprovalRequest; const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, @@ -386,6 +405,14 @@ export const secretApprovalRequestServiceFactory = ({ if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.secretApproval) { + throw new BadRequestError({ + message: + "Failed to merge secret approval request due to plan restriction. Upgrade plan to merge secret approval request." + }); + } + const { policy, folderId, projectId } = secretApprovalRequest; const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 910e81507..03a99bdd3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -356,7 +356,8 @@ export const registerRoutes = async ( projectEnvDAL, secretApprovalPolicyApproverDAL: sapApproverDAL, permissionService, - secretApprovalPolicyDAL + secretApprovalPolicyDAL, + licenseService }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); @@ -797,7 +798,8 @@ export const registerRoutes = async ( secretVersionTagV2BridgeDAL, smtpService, projectEnvDAL, - userDAL + userDAL, + licenseService }); const secretService = secretServiceFactory({