Merge remote-tracking branch 'origin' into certificate-alerting

This commit is contained in:
Tuan Dang
2024-08-17 19:47:37 -07:00
11 changed files with 257 additions and 126 deletions

View File

@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -61,6 +62,10 @@ export const auditLogServiceFactory = ({
}; };
const createAuditLog = async (data: TCreateAuditLogDTO) => { 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 // add all cases in which project id or org id cannot be added
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { 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" }); if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });

View File

@@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch"; import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; 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 { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import { import {
@@ -28,6 +29,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>; export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@@ -36,7 +38,8 @@ export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
permissionService, permissionService,
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
projectEnvDAL projectEnvDAL,
licenseService
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({ const createSecretApprovalPolicy = async ({
name, name,
@@ -65,6 +68,15 @@ export const secretApprovalPolicyServiceFactory = ({
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval 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 }); const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" }); if (!env) throw new BadRequestError({ message: "Environment not found" });
@@ -115,6 +127,14 @@ export const secretApprovalPolicyServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); 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 updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.updateById( const doc = await secretApprovalPolicyDAL.updateById(
secretApprovalPolicy.id, secretApprovalPolicy.id,
@@ -167,6 +187,14 @@ export const secretApprovalPolicyServiceFactory = ({
ProjectPermissionSub.SecretApproval 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); await secretApprovalPolicyDAL.deleteById(secretPolicyId);
return sapPolicy; return sapPolicy;
}; };

View File

@@ -50,6 +50,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
@@ -97,6 +98,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>; export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -122,7 +124,8 @@ export const secretApprovalRequestServiceFactory = ({
kmsService, kmsService,
secretV2BridgeDAL, secretV2BridgeDAL,
secretVersionV2BridgeDAL, secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL secretVersionTagV2BridgeDAL,
licenseService
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); 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 (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); 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 { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
@@ -345,6 +356,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); 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 { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
@@ -386,6 +405,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); 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 { policy, folderId, projectId } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,

View File

@@ -5,17 +5,26 @@ import { Redlock, Settings } from "@app/lib/red-lock";
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>; export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
// all the key prefixes used must be set here to avoid conflict // all the key prefixes used must be set here to avoid conflict
export enum KeyStorePrefixes { export const KeyStorePrefixes = {
SecretReplication = "secret-replication-import-lock", SecretReplication: "secret-replication-import-lock",
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock", KmsProjectDataKeyCreation: "kms-project-data-key-creation-lock",
KmsProjectKeyCreation = "kms-project-key-creation-lock", KmsProjectKeyCreation: "kms-project-key-creation-lock",
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-", WaitUntilReadyKmsProjectDataKeyCreation: "wait-until-ready-kms-project-data-key-creation-",
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-", WaitUntilReadyKmsProjectKeyCreation: "wait-until-ready-kms-project-key-creation-",
KmsOrgKeyCreation = "kms-org-key-creation-lock", KmsOrgKeyCreation: "kms-org-key-creation-lock",
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock", KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock",
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-", WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-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,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const
};
export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10
};
type TWaitTillReady = { type TWaitTillReady = {
key: string; key: string;
@@ -37,10 +46,10 @@ export const keyStoreFactory = (redisUrl: string) => {
const setItemWithExpiry = async ( const setItemWithExpiry = async (
key: string, key: string,
exp: number | string, expiryInSeconds: number | string,
value: string | number | Buffer, value: string | number | Buffer,
prefix?: string 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); const deleteItem = async (key: string) => redis.del(key);

View File

@@ -140,7 +140,8 @@ const envSchema = z
MAINTENANCE_MODE: zodStrBool.default("false"), MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional()), CAPTCHA_SECRET: zpStr(z.string().optional()),
PLAIN_API_KEY: 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) => ({ .transform((data) => ({
...data, ...data,

View File

@@ -1,2 +1,8 @@
export const getLastMidnightDateISO = (last = 1) => export const getLastMidnightDateISO = (last = 1) =>
`${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`; `${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);
};

View File

@@ -361,7 +361,8 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL, secretApprovalPolicyApproverDAL: sapApproverDAL,
permissionService, permissionService,
secretApprovalPolicyDAL secretApprovalPolicyDAL,
licenseService
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
@@ -739,6 +740,7 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const secretQueueService = secretQueueFactory({ const secretQueueService = secretQueueFactory({
keyStore,
queueService, queueService,
secretDAL, secretDAL,
folderDAL, folderDAL,
@@ -824,7 +826,8 @@ export const registerRoutes = async (
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
smtpService, smtpService,
projectEnvDAL, projectEnvDAL,
userDAL userDAL,
licenseService
}); });
const secretService = secretServiceFactory({ const secretService = secretServiceFactory({

View File

@@ -6,11 +6,12 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-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 { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { BadRequestError } from "@app/lib/errors"; 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 { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -79,6 +80,7 @@ type TSecretQueueFactoryDep = {
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">; secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">; snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">; snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
}; };
export type TGetSecrets = { export type TGetSecrets = {
@@ -122,7 +124,8 @@ export const secretQueueFactory = ({
secretRotationDAL, secretRotationDAL,
snapshotDAL, snapshotDAL,
snapshotSecretV2BridgeDAL, snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL secretApprovalRequestDAL,
keyStore
}: TSecretQueueFactoryDep) => { }: TSecretQueueFactoryDep) => {
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -576,7 +579,6 @@ export const secretQueueFactory = ({
) )
); );
} }
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -641,111 +643,157 @@ 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}]` `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 const lock = await keyStore.acquireLock(
? await getIntegrationSecretsV2({ [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
environment, 10000,
projectId, {
folderId: folder.id, retryCount: 3,
depth: 1, retryDelay: 2000
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 lockAcquiredTime = new Date();
const suffixedSecrets: typeof secrets = {}; const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
const metadata = integration.metadata as Record<string, string>; KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
if (metadata) { );
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
try { // check whether the integration should wait or not
// akhilmhdh: this needs to changed later to be more easier to use if (lastRunSyncIntegrationTimestamp) {
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places const INTEGRATION_INTERVAL = 2000;
const response = await syncIntegrationSecrets({ const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
createManySecretsRawFn, if (isStaleSyncIntegration) {
updateManySecretsRawFn, logger.info(
integrationDAL, `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}]`
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}]`
); );
return;
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
});
} }
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
lockAcquiredTime.toISOString(),
lastRunSyncIntegrationTimestamp
);
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && 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<string, string>;
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.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath),
KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds,
lockAcquiredTime.toISOString()
);
logger.info("Secret integration sync ended: %s", job.id); logger.info("Secret integration sync ended: %s", job.id);
}); });

View File

@@ -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: 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. 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. 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 ## Solution

View File

@@ -273,8 +273,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
const orgUsers = await fetchOrgUsers(currentOrg.id); const orgUsers = await fetchOrgUsers(currentOrg.id);
await addUsersToProject.mutateAsync({ await addUsersToProject.mutateAsync({
usernames: orgUsers usernames: orgUsers
.map((member) => member.user.username) .filter(
.filter((username) => username !== user.username), (member) => member.user.username !== user.username && member.status === "accepted"
)
.map((member) => member.user.username),
projectId: newProjectId, projectId: newProjectId,
orgId: currentOrg.id orgId: currentOrg.id
}); });
@@ -482,7 +484,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
)} )}
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior> <Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600"> <DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console Organization Admin Console
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />

View File

@@ -552,8 +552,10 @@ const OrganizationPage = () => {
await addUsersToProject.mutateAsync({ await addUsersToProject.mutateAsync({
usernames: orgUsers usernames: orgUsers
.map((member) => member.user.username) .filter(
.filter((username) => username !== user.username), (member) => member.user.username !== user.username && member.status === "accepted"
)
.map((member) => member.user.username),
projectId: newProjectId, projectId: newProjectId,
orgId: currentOrg.id orgId: currentOrg.id
}); });