Fix merge conflicts

This commit is contained in:
Tuan Dang
2024-08-18 12:05:15 -07:00
139 changed files with 2072 additions and 871 deletions
@@ -1,7 +1,7 @@
import * as x509 from "@peculiar/x509";
import crypto from "crypto";
import { BadRequestError } from "@app/lib/errors";
import { NotFoundError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
@@ -106,10 +106,10 @@ export const getCaCredentials = async ({
kmsService
}: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca) throw new NotFoundError({ message: "CA not found" });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
if (!caSecret) throw new BadRequestError({ message: "CA secret not found" });
if (!caSecret) throw new NotFoundError({ message: "CA secret not found" });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -158,7 +158,7 @@ export const getCaCertChains = async ({
kmsService
}: TGetCaCertChainsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca) throw new NotFoundError({ message: "CA not found" });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -205,7 +205,7 @@ export const getCaCertChain = async ({
kmsService
}: TGetCaCertChainDTO) => {
const caCert = await certificateAuthorityCertDAL.findById(caCertId);
if (!caCert) throw new BadRequestError({ message: "CA certificate not found" });
if (!caCert) throw new NotFoundError({ message: "CA certificate not found" });
const ca = await certificateAuthorityDAL.findById(caCert.caId);
const keyId = await getProjectKmsCertificateKeyId({
@@ -249,7 +249,7 @@ export const rebuildCaCrl = async ({
kmsService
}: TRebuildCaCrlDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca) throw new NotFoundError({ message: "CA not found" });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
@@ -23,5 +24,55 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
}
};
return { ...uaClientSecretOrm, incrementUsage };
const removeExpiredClientSecrets = async (tx?: Knex) => {
const BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
let deletedClientSecret: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
do {
try {
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
.where({
isClientSecretRevoked: true
})
.orWhere((qb) => {
void qb
.where("clientSecretNumUses", ">", 0)
.andWhere(
"clientSecretNumUses",
">=",
db.ref("clientSecretNumUsesLimit").withSchema(TableName.IdentityUaClientSecret)
);
})
.orWhere((qb) => {
void qb
.where("clientSecretTTL", ">", 0)
.andWhereRaw(
`"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()`
);
})
.select("id")
.limit(BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop
deletedClientSecret = await (tx || db)(TableName.IdentityUaClientSecret)
.whereIn("id", findExpiredClientSecretQuery)
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete client secret on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
};
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };
};
@@ -0,0 +1,357 @@
import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
import { Integrations } from "./integration-list";
const MAX_SYNC_SECRET_DEPTH = 5;
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV2 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
depth: number;
decryptor: (value: Buffer | null | undefined) => string;
},
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
const content: Record<string, boolean> = {};
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 secretV2BridgeDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = secret.key;
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
if (!secretImports.length) return content;
const importedSecrets = await fnSecretsV2FromImports({
decryptor: dto.decryptor,
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
allowedImports: secretImports
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (!content[importedSecret.key]) {
content[importedSecret.key] = true;
}
}
}
return content;
};
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV1 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
key: string;
depth: number;
},
secretDAL: Pick<TSecretDALFactory, "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
let content: Record<string, boolean> = {};
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);
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: dto.key
});
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
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) {
// get secrets contained in each imported folder by recursively calling
// this function against the imported folder
const importedSecrets = await getIntegrationSecretsV1(
{
environment: dto.environment,
projectId: dto.projectId,
folderId: folder.id,
key: dto.key,
depth: dto.depth + 1
},
secretDAL,
folderDAL,
secretImportDAL
);
// add the imported secrets to the current folder secrets
content = { ...importedSecrets, ...content };
}
}
return content;
};
export const deleteGithubSecrets = async ({
integration,
secrets,
accessToken
}: {
integration: Omit<TIntegrations, "envId">;
secrets: Record<string, boolean>;
accessToken: string;
}) => {
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
visibility?: "all" | "private" | "selected";
selected_repositories_url?: string | undefined;
}
const OctokitWithRetry = Octokit.plugin(retry);
const octokit = new OctokitWithRetry({
auth: accessToken
});
enum GithubScope {
Repo = "github-repo",
Org = "github-org",
Env = "github-env"
}
let encryptedGithubSecrets: GitHubSecret[];
switch (integration.scope) {
case GithubScope.Org: {
encryptedGithubSecrets = (
await octokit.request("GET /orgs/{org}/actions/secrets", {
org: integration.owner as string
})
).data.secrets;
break;
}
case GithubScope.Env: {
encryptedGithubSecrets = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string
})
).data.secrets;
break;
}
default: {
encryptedGithubSecrets = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
repo: integration.app as string
})
).data.secrets;
break;
}
}
for await (const encryptedSecret of encryptedGithubSecrets) {
if (encryptedSecret.name in secrets) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
}
// small delay to prevent hitting API rate limits
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
}
}
};
export const deleteIntegrationSecrets = async ({
integration,
integrationAuth,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
folderDAL,
secretDAL,
secretImportDAL,
kmsService
}: {
integration: Omit<TIntegrations, "envId"> & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken" | "getIntegrationAuth">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integration.projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: integration.projectId
});
const folder = await folderDAL.findBySecretPath(
integration.projectId,
integration.environment.slug,
integration.secretPath
);
if (!folder) {
throw new NotFoundError({
message: "Folder not found."
});
}
const { accessToken } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
shouldUseSecretV2Bridge,
botKey
);
const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
depth: 1,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
},
secretV2BridgeDAL,
folderDAL,
secretImportDAL
)
: await getIntegrationSecretsV1(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
},
secretDAL,
folderDAL,
secretImportDAL
);
const suffixedSecrets: typeof secrets = {};
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
switch (integration.integration) {
case Integrations.GITHUB: {
await deleteGithubSecrets({
integration,
accessToken,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
});
break;
}
default:
throw new BadRequestError({
message: "Invalid integration"
});
}
};
@@ -538,19 +538,20 @@ const syncSecretsAWSParameterStore = async ({
integration,
secrets,
accessId,
accessToken
accessToken,
projectId
}: {
integration: TIntegrations;
integration: TIntegrations & { secretPath: string; environment: { slug: string } };
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
accessToken: string;
projectId?: string;
}) => {
let response: { isSynced: boolean; syncMessage: string } | null = null;
if (!accessId) {
throw new Error("AWS access ID is required");
}
const config = new AWS.Config({
region: integration.region as string,
credentials: {
@@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({
const metadata = z.record(z.any()).parse(integration.metadata || {});
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
logger.info(
`getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]`
);
// now fetch all aws parameter store secrets
let hasNext = true;
let nextToken: string | undefined;
@@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({
nextToken = parameters.NextToken;
}
logger.info(
`getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys(
awsParameterStoreSecretsObj
).join(",")}]`
);
logger.info(
`getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]`
);
// Identify secrets to create
// don't use Promise.all() and promise map here
// it will cause rate limit
@@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({
// case: secret does not exist in AWS parameter store
// -> create secret
if (secrets[key].value) {
logger.info(
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
// Overwrite: true,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
Overwrite: true
})
.promise();
if (metadata.secretAWSTag?.length) {
try {
await ssm
.addTagsToResource({
ResourceType: "Parameter",
ResourceId: `${integration.path}${key}`,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
})
.promise();
} catch (err) {
logger.error(
err,
`getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
logger.error(
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
);
}
response = {
isSynced: false,
syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store"
};
}
}
}
// case: secret exists in AWS parameter store
} else {
logger.info(
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// -> update secret
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
await ssm
@@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({
})
.promise();
} catch (err) {
logger.error(
err,
`getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
logger.error(
@@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({
}
if (!metadata.shouldDisableDelete) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=1]`
);
for (const key in awsParameterStoreSecretsObj) {
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]`
);
if (!(key in secrets)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]`
);
// case:
// -> delete secret
await ssm
@@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({
Name: awsParameterStoreSecretsObj[key].Name as string
})
.promise();
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]`
);
}
await new Promise((resolve) => {
setTimeout(resolve, 50);
@@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({
integration,
secrets,
accessId,
accessToken
accessToken,
projectId
});
break;
case Integrations.AWS_SECRET_MANAGER:
@@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
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/secret-import-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationDALFactory } from "./integration-dal";
import {
TCreateIntegrationDTO,
@@ -19,9 +26,15 @@ import {
type TIntegrationServiceFactoryDep = {
integrationDAL: TIntegrationDALFactory;
integrationAuthDAL: TIntegrationAuthDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
integrationAuthService: TIntegrationAuthServiceFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectBotService: TProjectBotServiceFactory;
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
};
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
@@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
integrationAuthDAL,
folderDAL,
permissionService,
secretQueueService
secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
kmsService,
secretDAL
}: TIntegrationServiceFactoryDep) => {
const createIntegration = async ({
app,
@@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
return updatedIntegration;
};
const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => {
const deleteIntegration = async ({
actorId,
id,
actor,
actorAuthMethod,
actorOrgId,
shouldDeleteIntegrationSecrets
}: TDeleteIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
@@ -174,6 +200,22 @@ export const integrationServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
if (shouldDeleteIntegrationSecrets) {
await deleteIntegrationSecrets({
integration,
integrationAuth,
projectBotService,
integrationAuthService,
secretV2BridgeDAL,
folderDAL,
secretImportDAL,
secretDAL,
kmsService
});
}
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
// delete integration
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
@@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
export type TDeleteIntegrationDTO = {
id: string;
shouldDeleteIntegrationSecrets?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TSyncIntegrationDTO = {
@@ -0,0 +1,48 @@
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
type TDailyExpiringPkiItemAlertQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
};
export type TDailyExpiringPkiItemAlertQueueServiceFactory = ReturnType<
typeof dailyExpiringPkiItemAlertQueueServiceFactory
>;
export const dailyExpiringPkiItemAlertQueueServiceFactory = ({
queueService,
pkiAlertService
}: TDailyExpiringPkiItemAlertQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyExpiringPkiItemAlert, async () => {
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task started`);
await pkiAlertService.sendPkiItemExpiryNotices();
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task completed`);
});
// we do a repeat cron job in utc timezone at 12 Midnight each day
const startSendingAlerts = async () => {
// clear previous job
await queueService.stopRepeatableJob(
QueueName.DailyExpiringPkiItemAlert,
QueueJobs.DailyExpiringPkiItemAlert,
{ pattern: "0 0 * * *", utc: true },
QueueName.DailyExpiringPkiItemAlert // just a job id
);
await queueService.queue(QueueName.DailyExpiringPkiItemAlert, QueueJobs.DailyExpiringPkiItemAlert, undefined, {
delay: 5000,
jobId: QueueName.DailyExpiringPkiItemAlert,
repeat: { pattern: "0 0 * * *", utc: true }
});
};
queueService.listen(QueueName.DailyExpiringPkiItemAlert, "failed", (_, err) => {
logger.error(err, `${QueueName.DailyExpiringPkiItemAlert}: Expiring PKI item alert failed`);
});
return {
startSendingAlerts
};
};
@@ -12,8 +12,11 @@ import { TPkiAlertDALFactory } from "./pki-alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
type TPkiAlertServiceFactoryDep = {
pkiAlertDAL: TPkiAlertDALFactory;
pkiCollectionDAL: TPkiCollectionDALFactory;
pkiAlertDAL: Pick<
TPkiAlertDALFactory,
"create" | "findById" | "updateById" | "deleteById" | "getExpiringPkiCollectionItemsForAlerting"
>;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: Pick<TSmtpService, "sendMail">;
};
@@ -81,7 +81,7 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count all project certificates" });
throw new DatabaseError({ error, name: "Count all PKI collection items" });
}
};
@@ -22,10 +22,13 @@ import {
} from "./pki-collection-types";
type TPkiCollectionServiceFactoryDep = {
pkiCollectionDAL: TPkiCollectionDALFactory; // TODO: Pick
pkiCollectionItemDAL: TPkiCollectionItemDALFactory;
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
certificateDAL: TCertificateDALFactory;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "create" | "findById" | "updateById" | "deleteById">;
pkiCollectionItemDAL: Pick<
TPkiCollectionItemDALFactory,
"findOne" | "create" | "deleteById" | "findPkiCollectionItems" | "countItemsInPkiCollection"
>;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@@ -139,7 +142,7 @@ export const pkiCollectionServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete,
ProjectPermissionSub.PkiCollections
);
pkiCollection = await pkiCollectionDAL.deleteById(collectionId);
@@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
const doc = await db
.replicaNode()(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
.where(`${TableName.Users}.isGhost` as "isGhost", false)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
@@ -2,9 +2,9 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
@@ -13,13 +13,13 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
queueService: TQueueServiceFactory;
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
};
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
@@ -27,24 +27,24 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
export const dailyResourceCleanUpQueueServiceFactory = ({
auditLogDAL,
queueService,
pkiAlertService,
snapshotDAL,
secretVersionDAL,
secretFolderVersionDAL,
identityAccessTokenDAL,
secretSharingDAL,
secretVersionV2DAL
secretVersionV2DAL,
identityUniversalAuthClientSecretDAL
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await auditLogDAL.pruneAuditLog();
await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets();
await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions();
await pkiAlertService.sendPkiItemExpiryNotices();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
});
@@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = {
secretKey: string;
// akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string | undefined;
secretComment: string | undefined;
secretValue: string;
secretComment: string;
})[];
};
@@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
depth?: number;
cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string | undefined;
decryptor: (value?: Buffer | null) => string;
expandSecretReferences?: (
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
@@ -231,6 +231,7 @@ export const fnSecretsV2FromImports = async ({
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
}))
.concat(folderDeeperImportSecrets);
return {
secretPath: importPath,
environment: importEnv.slug,
@@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({
};
return acc;
},
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
{} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey);
@@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
});
return importedSecrets;
}
@@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany,
deleteTagsToSecret: secretJnTagOrm.delete,
saveTagsToSecretV2: secretV2JnTagOrm.insertMany,
saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
findSecretTagsByProjectId,
deleteTagsManySecret,
@@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
const createTag = async ({
name,
slug,
actor,
color,
actorId,
actorOrgId,
actorAuthMethod,
projectId
}: TCreateTagDTO) => {
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
@@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
const newTag = await secretTagDAL.create({
projectId,
name,
slug,
color,
createdBy: actorId,
@@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return newTag;
};
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => {
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, color, slug }: TUpdateTagDTO) => {
const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
@@ -73,7 +63,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug });
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
return updatedTag;
};
@@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag;
return { ...tag, name: tag.slug };
};
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
@@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag;
return { ...tag, name: tag.slug };
};
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
@@ -1,14 +1,12 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateTagDTO = {
name: string;
color: string;
slug: string;
} & TProjectPermission;
export type TUpdateTagDTO = {
id: string;
name?: string;
slug?: string;
color?: string;
} & Omit<TProjectPermission, "projectId">;
@@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
@@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.where({ [`${TableName.SecretV2}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
return tags.map((el) => ({
id: el.tagId,
color: el.tagColor,
slug: el.tagSlug,
name: el.tagName
name: el.tagSlug
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret tags" });
@@ -210,7 +208,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
@@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@@ -290,7 +287,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
}))
);
if (!newSecretReferences.length) return;
const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences);
const secretReferences = await (tx || db).batchInsert(TableName.SecretReferenceV2, newSecretReferences);
return secretReferences;
} catch (error) {
throw new DatabaseError({ error, name: "UpsertSecretReference" });
@@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
@@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@@ -528,8 +528,8 @@ export const reshapeBridgeSecret = (
environment: string,
secretPath: string,
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value?: string;
comment?: string;
value: string;
comment: string;
tags?: {
id: string;
slug: string;
@@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
secretPath,
workspace: workspaceId,
environment,
secretValue: secret.value,
secretComment: secret.comment,
secretValue: secret.value || "",
secretComment: secret.comment || "",
version: secret.version,
type: secret.type,
_id: secret.id,
@@ -196,7 +196,7 @@ export const secretV2BridgeServiceFactory = ({
return reshapeBridgeSecret(projectId, environment, secretPath, {
...secret[0],
value: inputSecret.secretValue,
comment: inputSecret.secretComment
comment: inputSecret.secretComment || ""
});
};
@@ -339,8 +339,8 @@ export const secretV2BridgeServiceFactory = ({
});
return reshapeBridgeSecret(projectId, environment, secretPath, {
...updatedSecret[0],
value: inputSecret.secretValue,
comment: inputSecret.secretComment
value: inputSecret.secretValue || "",
comment: inputSecret.secretComment || ""
});
};
@@ -378,6 +378,18 @@ export const secretV2BridgeServiceFactory = ({
throw new BadRequestError({ message: "Must be user to delete personal secret" });
}
const secretToDelete = await secretDAL.findOne({
key: inputSecret.secretName,
folderId,
...(inputSecret.type === SecretType.Shared
? {}
: {
type: SecretType.Personal,
userId: actorId
})
});
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
const deletedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({
projectId,
@@ -412,10 +424,10 @@ export const secretV2BridgeServiceFactory = ({
...deletedSecret[0],
value: deletedSecret[0].encryptedValue
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
: undefined,
: "",
comment: deletedSecret[0].encryptedComment
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
: undefined
: ""
});
};
@@ -429,6 +441,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
includeImports,
recursive,
tagSlugs = [],
expandSecretReferences: shouldExpandSecretReferences
}: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission(
@@ -496,6 +509,9 @@ export const secretV2BridgeServiceFactory = ({
: ""
})
);
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const expandSecretReferences = expandSecretReferencesFactory({
projectId,
folderDAL,
@@ -504,7 +520,7 @@ export const secretV2BridgeServiceFactory = ({
});
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
for (const secretPathKey in secretsGroupByPath) {
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
@@ -522,7 +538,7 @@ export const secretV2BridgeServiceFactory = ({
await expandSecretReferences(secretsGroupByKey);
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
});
}
}
@@ -530,7 +546,7 @@ export const secretV2BridgeServiceFactory = ({
if (!includeImports) {
return {
secrets: decryptedSecrets
secrets: filteredSecrets
};
}
@@ -554,11 +570,11 @@ export const secretV2BridgeServiceFactory = ({
folderDAL,
secretImportDAL,
expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
});
return {
secrets: decryptedSecrets,
secrets: filteredSecrets,
imports: importedSecrets
};
};
@@ -654,7 +670,7 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
});
@@ -662,12 +678,11 @@ export const secretV2BridgeServiceFactory = ({
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (secretName === importedSecret.key) {
return reshapeBridgeSecret(
projectId,
importedSecrets[i].environment,
importedSecrets[i].secretPath,
importedSecret
);
return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
...importedSecret,
value: importedSecret.secretValue || "",
comment: importedSecret.secretComment || ""
});
}
}
}
@@ -676,7 +691,7 @@ export const secretV2BridgeServiceFactory = ({
let secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: undefined;
: "";
if (shouldExpandSecretReferences && secretValue) {
const secretReferenceExpandedRecord = {
[secret.key]: { value: secretValue }
@@ -691,7 +706,7 @@ export const secretV2BridgeServiceFactory = ({
value: secretValue,
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: undefined
: ""
});
};
@@ -781,10 +796,8 @@ export const secretV2BridgeServiceFactory = ({
return newSecrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};
@@ -902,10 +915,8 @@ export const secretV2BridgeServiceFactory = ({
return secrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};
@@ -981,10 +992,8 @@ export const secretV2BridgeServiceFactory = ({
return secretsDeleted.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};
@@ -1020,10 +1029,8 @@ export const secretV2BridgeServiceFactory = ({
return secretVersions.map((el) =>
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};
@@ -20,6 +20,7 @@ export type TGetSecretsDTO = {
environment: string;
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission;
export type TGetASecretDTO = {
+9 -13
View File
@@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
@@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => {
.where({ [`${TableName.Secret}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
return tags.map((el) => ({
id: el.tagId,
color: el.tagColor,
slug: el.tagSlug,
name: el.tagName
name: el.tagSlug
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret tags" });
@@ -188,7 +186,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
@@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
@@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
+1 -2
View File
@@ -370,7 +370,6 @@ export const decryptSecretRaw = (
id: string;
slug: string;
color?: string | null;
name: string;
}[];
},
key: string
@@ -412,7 +411,7 @@ export const decryptSecretRaw = (
_id: secret.id,
id: secret.id,
user: secret.userId,
tags: secret.tags,
tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote,
+157 -106
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 { 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";
@@ -73,12 +74,13 @@ type TSecretQueueFactoryDep = {
secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
};
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,108 +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}]`
);
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
});
const lock = await keyStore.acquireLock(
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
10000,
{
retryCount: 3,
retryDelay: 2000
}
);
const lockAcquiredTime = new Date();
for (const integration of toBeSyncedIntegrations) {
const integrationAuth = {
...integration.integrationAuth,
createdAt: new Date(),
updatedAt: new Date(),
projectId: integration.projectId
};
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
);
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
shouldUseSecretV2Bridge,
botKey
// 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(
`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}]`
);
return;
}
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
lockAcquiredTime.toISOString(),
lastRunSyncIntegrationTimestamp
);
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
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
await new Promise((resolve) => {
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
});
}
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];
});
}
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.info("Secret integration sync error: %o", err);
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
});
}
}
// 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);
});
@@ -828,7 +879,7 @@ export const secretQueueFactory = ({
secretId: string;
references: { environment: string; secretPath: string; secretKey: string }[];
}[] = [];
await secretV2BridgeDAL.insertMany(
await secretV2BridgeDAL.batchInsert(
projectV1Secrets.map((el) => {
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext,
@@ -1004,14 +1055,14 @@ export const secretQueueFactory = ({
const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById);
if (projectV3SecretVersions.length) {
await secretVersionV2BridgeDAL.insertMany(projectV3SecretVersions, tx);
await secretVersionV2BridgeDAL.batchInsert(projectV3SecretVersions, tx);
}
if (projectV3SecretVersionTags.length) {
await secretVersionTagV2BridgeDAL.insertMany(projectV3SecretVersionTags, tx);
await secretVersionTagV2BridgeDAL.batchInsert(projectV3SecretVersionTags, tx);
}
if (projectV3SnapshotSecrets.length) {
await snapshotSecretV2BridgeDAL.insertMany(projectV3SnapshotSecrets, tx);
await snapshotSecretV2BridgeDAL.batchInsert(projectV3SnapshotSecrets, tx);
}
await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx);
}
+11 -5
View File
@@ -964,7 +964,8 @@ export const secretServiceFactory = ({
environment,
includeImports,
expandSecretReferences,
recursive
recursive,
tagSlugs = []
}: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
@@ -978,7 +979,8 @@ export const secretServiceFactory = ({
path,
recursive,
actorAuthMethod,
includeImports
includeImports,
tagSlugs
});
return { secrets, imports };
}
@@ -998,6 +1000,9 @@ export const secretServiceFactory = ({
});
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw(
@@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({
};
// expand secrets
await batchSecretsExpand(decryptedSecrets);
await batchSecretsExpand(filteredSecrets);
// expand imports by batch
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
}
return {
secrets: decryptedSecrets,
secrets: filteredSecrets,
imports: processedImports
};
};
@@ -1149,6 +1154,7 @@ export const secretServiceFactory = ({
type,
secretName
});
return secret;
}
@@ -2081,7 +2087,7 @@ export const secretServiceFactory = ({
return {
...updatedSecret[0],
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.slug, color: t.color }))
};
};
@@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = {
environment: string;
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission;
export type TGetASecretRawDTO = {