mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 06:35:21 +00:00
Fix merge conflicts
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user