mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 08:35:52 +00:00
Merge remote-tracking branch 'origin/main' into misc/metrics-observability
This commit is contained in:
Vendored
+20
-1
@@ -98,6 +98,15 @@ import {
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate,
|
||||
TKmsKeys,
|
||||
TKmsKeysInsert,
|
||||
TKmsKeysUpdate,
|
||||
TKmsKeyVersions,
|
||||
TKmsKeyVersionsInsert,
|
||||
TKmsKeyVersionsUpdate,
|
||||
TKmsRootConfig,
|
||||
TKmsRootConfigInsert,
|
||||
TKmsRootConfigUpdate,
|
||||
TLdapConfigs,
|
||||
TLdapConfigsInsert,
|
||||
TLdapConfigsUpdate,
|
||||
@@ -176,6 +185,9 @@ import {
|
||||
TSecretImports,
|
||||
TSecretImportsInsert,
|
||||
TSecretImportsUpdate,
|
||||
TSecretReferences,
|
||||
TSecretReferencesInsert,
|
||||
TSecretReferencesUpdate,
|
||||
TSecretRotationOutputs,
|
||||
TSecretRotationOutputsInsert,
|
||||
TSecretRotationOutputsUpdate,
|
||||
@@ -240,7 +252,6 @@ import {
|
||||
TWebhooksInsert,
|
||||
TWebhooksUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
|
||||
|
||||
declare module "knex/types/tables" {
|
||||
interface Tables {
|
||||
@@ -514,5 +525,13 @@ declare module "knex/types/tables" {
|
||||
TSecretVersionTagJunctionInsert,
|
||||
TSecretVersionTagJunctionUpdate
|
||||
>;
|
||||
// KMS service
|
||||
[TableName.KmsServerRootConfig]: Knex.CompositeTableType<
|
||||
TKmsRootConfig,
|
||||
TKmsRootConfigInsert,
|
||||
TKmsRootConfigUpdate
|
||||
>;
|
||||
[TableName.KmsKey]: Knex.CompositeTableType<TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate>;
|
||||
[TableName.KmsKeyVersion]: Knex.CompositeTableType<TKmsKeyVersions, TKmsKeyVersionsInsert, TKmsKeyVersionsUpdate>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.KmsServerRootConfig))) {
|
||||
await knex.schema.createTable(TableName.KmsServerRootConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("encryptedRootKey").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.KmsKey))) {
|
||||
await knex.schema.createTable(TableName.KmsKey, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("encryptedKey").notNullable();
|
||||
t.string("encryptionAlgorithm").notNullable();
|
||||
t.integer("version").defaultTo(1).notNullable();
|
||||
t.string("description");
|
||||
t.boolean("isDisabled").defaultTo(false);
|
||||
t.boolean("isReserved").defaultTo(true);
|
||||
t.string("projectId");
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.KmsKey);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.KmsKeyVersion))) {
|
||||
await knex.schema.createTable(TableName.KmsKeyVersion, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("encryptedKey").notNullable();
|
||||
t.integer("version").notNullable();
|
||||
t.uuid("kmsKeyId").notNullable();
|
||||
t.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.KmsKeyVersion);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.KmsServerRootConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.KmsKeyVersion);
|
||||
await dropOnUpdateTrigger(knex, TableName.KmsKeyVersion);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.KmsKey);
|
||||
await dropOnUpdateTrigger(knex, TableName.KmsKey);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
|
||||
TableName.Users,
|
||||
"consecutiveFailedPasswordAttempts"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Users, (tb) => {
|
||||
if (!hasConsecutiveFailedPasswordAttempts) {
|
||||
tb.integer("consecutiveFailedPasswordAttempts").defaultTo(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
|
||||
TableName.Users,
|
||||
"consecutiveFailedPasswordAttempts"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Users, (tb) => {
|
||||
if (hasConsecutiveFailedPasswordAttempts) {
|
||||
tb.dropColumn("consecutiveFailedPasswordAttempts");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -30,6 +30,9 @@ export * from "./identity-universal-auths";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./kms-key-versions";
|
||||
export * from "./kms-keys";
|
||||
export * from "./kms-root-config";
|
||||
export * from "./ldap-configs";
|
||||
export * from "./ldap-group-maps";
|
||||
export * from "./models";
|
||||
@@ -57,6 +60,7 @@ export * from "./secret-blind-indexes";
|
||||
export * from "./secret-folder-versions";
|
||||
export * from "./secret-folders";
|
||||
export * from "./secret-imports";
|
||||
export * from "./secret-references";
|
||||
export * from "./secret-rotation-outputs";
|
||||
export * from "./secret-rotations";
|
||||
export * from "./secret-scanning-git-risks";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const KmsKeyVersionsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
version: z.number(),
|
||||
kmsKeyId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TKmsKeyVersions = z.infer<typeof KmsKeyVersionsSchema>;
|
||||
export type TKmsKeyVersionsInsert = Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>;
|
||||
export type TKmsKeyVersionsUpdate = Partial<Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const KmsKeysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
encryptionAlgorithm: z.string(),
|
||||
version: z.number().default(1),
|
||||
description: z.string().nullable().optional(),
|
||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||
isReserved: z.boolean().default(true).nullable().optional(),
|
||||
projectId: z.string().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
export type TKmsKeysInsert = Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>;
|
||||
export type TKmsKeysUpdate = Partial<Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const KmsRootConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedRootKey: zodBuffer
|
||||
});
|
||||
|
||||
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
|
||||
export type TKmsRootConfigInsert = Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>;
|
||||
export type TKmsRootConfigUpdate = Partial<Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>>;
|
||||
@@ -81,7 +81,11 @@ export enum TableName {
|
||||
DynamicSecretLease = "dynamic_secret_leases",
|
||||
// junction tables with tags
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
SecretVersionTag = "secret_version_tag_junction"
|
||||
SecretVersionTag = "secret_version_tag_junction",
|
||||
// KMS Service
|
||||
KmsServerRootConfig = "kms_root_config",
|
||||
KmsKey = "kms_keys",
|
||||
KmsKeyVersion = "kms_key_versions"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
||||
@@ -25,7 +25,8 @@ export const UsersSchema = z.object({
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||
isLocked: z.boolean().default(false).nullable().optional(),
|
||||
temporaryLockDateEnd: z.date().nullable().optional()
|
||||
temporaryLockDateEnd: z.date().nullable().optional(),
|
||||
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
|
||||
@@ -77,7 +77,7 @@ type TLdapConfigServiceFactoryDep = {
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
};
|
||||
|
||||
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
||||
@@ -510,6 +510,7 @@ export const ldapConfigServiceFactory = ({
|
||||
return newUserAlias;
|
||||
});
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const user = await userDAL.transaction(async (tx) => {
|
||||
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||
|
||||
@@ -50,7 +50,7 @@ type TSamlConfigServiceFactoryDep = {
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
@@ -449,6 +449,7 @@ export const samlConfigServiceFactory = ({
|
||||
return newUser;
|
||||
});
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
const providerAuthToken = jwt.sign(
|
||||
|
||||
@@ -391,7 +391,7 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(org.id);
|
||||
return { user, orgMembership };
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,15 @@ export enum KeyStorePrefixes {
|
||||
SecretReplication = "secret-replication-import-lock"
|
||||
}
|
||||
|
||||
type TWaitTillReady = {
|
||||
key: string;
|
||||
waitingCb?: () => void;
|
||||
keyCheckCb: (val: string | null) => boolean;
|
||||
waitIteration?: number;
|
||||
delay?: number;
|
||||
jitter?: number;
|
||||
};
|
||||
|
||||
export const keyStoreFactory = (redisUrl: string) => {
|
||||
const redis = new Redis(redisUrl);
|
||||
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
||||
@@ -29,6 +38,29 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
|
||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||
|
||||
const waitTillReady = async ({
|
||||
key,
|
||||
waitingCb,
|
||||
keyCheckCb,
|
||||
waitIteration = 10,
|
||||
delay = 1000,
|
||||
jitter = 200
|
||||
}: TWaitTillReady) => {
|
||||
let attempts = 0;
|
||||
let isReady = keyCheckCb(await getItem(key));
|
||||
while (!isReady) {
|
||||
if (attempts > waitIteration) return;
|
||||
// eslint-disable-next-line
|
||||
await new Promise((resolve) => {
|
||||
waitingCb?.();
|
||||
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
|
||||
});
|
||||
attempts += 1;
|
||||
// eslint-disable-next-line
|
||||
isReady = keyCheckCb(await getItem(key, "wait_till_ready"));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
setItem,
|
||||
getItem,
|
||||
@@ -37,6 +69,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
incrementBy,
|
||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||
return redisLock.acquire(resources, duration, settings);
|
||||
}
|
||||
},
|
||||
waitTillReady
|
||||
};
|
||||
};
|
||||
|
||||
@@ -386,6 +386,8 @@ export const SECRET_IMPORTS = {
|
||||
environment: "The slug of the environment to import into.",
|
||||
path: "The path to import into.",
|
||||
workspaceId: "The ID of the project you are working in.",
|
||||
isReplication:
|
||||
"When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.",
|
||||
import: {
|
||||
environment: "The slug of the environment to import from.",
|
||||
path: "The path to import from."
|
||||
@@ -661,6 +663,7 @@ export const INTEGRATION = {
|
||||
targetServiceId:
|
||||
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
|
||||
owner: "External integration providers service entity owner. Used in Github.",
|
||||
url: "The self-hosted URL of the platform to integrate with",
|
||||
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
|
||||
region: "AWS region to sync secrets to.",
|
||||
scope: "Scope of the provider. Used by Github, Qovery",
|
||||
@@ -673,7 +676,10 @@ export const INTEGRATION = {
|
||||
secretGCPLabel: "The label for GCP secrets.",
|
||||
secretAWSTag: "The tags for AWS secrets.",
|
||||
kmsKeyId: "The ID of the encryption key from AWS KMS.",
|
||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store."
|
||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
|
||||
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
|
||||
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
|
||||
shouldEnableDelete: "The flag to enable deletion of secrets"
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
|
||||
@@ -39,7 +39,9 @@ const envSchema = z
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
SMTP_SECURE: zodStrBool,
|
||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||
SMTP_REQUIRE_TLS: zodStrBool.default("true"),
|
||||
SMTP_TLS_REJECT_UNAUTHORIZED: zodStrBool.default("true"),
|
||||
SMTP_PORT: z.coerce.number().default(587),
|
||||
SMTP_USERNAME: zpStr(z.string().optional()),
|
||||
SMTP_PASSWORD: zpStr(z.string().optional()),
|
||||
@@ -75,6 +77,7 @@ const envSchema = z
|
||||
.optional()
|
||||
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
|
||||
), // fallback since URL_GITLAB_LOGIN has been renamed
|
||||
DEFAULT_SAML_ORG_SLUG: zpStr(z.string().optional()).default(process.env.NEXT_PUBLIC_SAML_ORG_SLUG),
|
||||
// integration client secrets
|
||||
// heroku
|
||||
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
|
||||
@@ -120,6 +123,7 @@ const envSchema = z
|
||||
.optional(),
|
||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
||||
OTEL_COLLECTOR_OTLP_URL: zpStr(z.string().optional())
|
||||
})
|
||||
@@ -133,7 +137,8 @@ const envSchema = z
|
||||
isSecretScanningConfigured:
|
||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET)
|
||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
||||
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
|
||||
}));
|
||||
|
||||
let envCfg: Readonly<z.infer<typeof envSchema>>;
|
||||
@@ -152,13 +157,20 @@ export const initEnvConfig = (logger: Logger) => {
|
||||
return envCfg;
|
||||
};
|
||||
|
||||
export const formatSmtpConfig = () => ({
|
||||
host: envCfg.SMTP_HOST,
|
||||
port: envCfg.SMTP_PORT,
|
||||
auth:
|
||||
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
|
||||
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
|
||||
: undefined,
|
||||
secure: envCfg.SMTP_SECURE,
|
||||
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`
|
||||
});
|
||||
export const formatSmtpConfig = () => {
|
||||
return {
|
||||
host: envCfg.SMTP_HOST,
|
||||
port: envCfg.SMTP_PORT,
|
||||
auth:
|
||||
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
|
||||
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
|
||||
: undefined,
|
||||
secure: envCfg.SMTP_PORT === 465,
|
||||
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`,
|
||||
ignoreTLS: envCfg.SMTP_IGNORE_TLS,
|
||||
requireTLS: envCfg.SMTP_REQUIRE_TLS,
|
||||
tls: {
|
||||
rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
||||
|
||||
const getIvLength = () => {
|
||||
return 12;
|
||||
};
|
||||
|
||||
const getTagLength = () => {
|
||||
return 16;
|
||||
};
|
||||
|
||||
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
|
||||
const IV_LENGTH = getIvLength();
|
||||
const TAG_LENGTH = getTagLength();
|
||||
|
||||
const encrypt = (text: Buffer, key: Buffer) => {
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(type, key, iv);
|
||||
|
||||
let encrypted = cipher.update(text);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
|
||||
// Get the authentication tag
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
// Concatenate IV, encrypted text, and tag into a single buffer
|
||||
const ciphertextBlob = Buffer.concat([iv, encrypted, tag]);
|
||||
return ciphertextBlob;
|
||||
};
|
||||
|
||||
const decrypt = (ciphertextBlob: Buffer, key: Buffer) => {
|
||||
// Extract the IV, encrypted text, and tag from the buffer
|
||||
const iv = ciphertextBlob.subarray(0, IV_LENGTH);
|
||||
const tag = ciphertextBlob.subarray(-TAG_LENGTH);
|
||||
const encrypted = ciphertextBlob.subarray(IV_LENGTH, -TAG_LENGTH);
|
||||
|
||||
const decipher = crypto.createDecipheriv(type, key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
return {
|
||||
encrypt,
|
||||
decrypt
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { symmetricCipherService } from "./cipher";
|
||||
export { SymmetricEncryption } from "./types";
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum SymmetricEncryption {
|
||||
AES_GCM_256 = "aes-256-gcm",
|
||||
AES_GCM_128 = "aes-128-gcm"
|
||||
}
|
||||
|
||||
export type TSymmetricEncryptionFns = {
|
||||
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||
decrypt: (blob: Buffer, key: Buffer) => Buffer;
|
||||
};
|
||||
@@ -11,6 +11,8 @@ import { getConfig } from "../config/env";
|
||||
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
||||
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
||||
|
||||
export const randomSecureBytes = (length = 32) => crypto.randomBytes(length);
|
||||
|
||||
export type TDecryptSymmetricInput = {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
|
||||
@@ -9,7 +9,8 @@ export {
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateAsymmetricKeyPair
|
||||
generateAsymmetricKeyPair,
|
||||
randomSecureBytes
|
||||
} from "./encryption";
|
||||
export {
|
||||
decryptIntegrationAuths,
|
||||
|
||||
@@ -7,3 +7,7 @@ export const zpStr = <T extends ZodTypeAny>(schema: T, opt: { stripNull: boolean
|
||||
if (typeof val !== "string") return val;
|
||||
return val.trim() || undefined;
|
||||
}, schema);
|
||||
|
||||
export const zodBuffer = z.custom<Buffer>((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, {
|
||||
message: "Expected binary data (Buffer Or Uint8Array)"
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { createTransport } from "nodemailer";
|
||||
|
||||
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getTlsOption } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
type BootstrapOpt = {
|
||||
@@ -44,7 +43,7 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
|
||||
console.info("Testing smtp connection");
|
||||
|
||||
const smtpCfg = formatSmtpConfig();
|
||||
await createTransport({ ...smtpCfg, ...getTlsOption(smtpCfg.host, smtpCfg.secure) })
|
||||
await createTransport(smtpCfg)
|
||||
.verify()
|
||||
.then(async () => {
|
||||
console.info("SMTP successfully connected");
|
||||
|
||||
@@ -97,6 +97,9 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal
|
||||
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { kmsDALFactory } from "@app/services/kms/kms-dal";
|
||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||
@@ -261,6 +264,9 @@ export const registerRoutes = async (
|
||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||
|
||||
const kmsDAL = kmsDALFactory(db);
|
||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@@ -269,6 +275,12 @@ export const registerRoutes = async (
|
||||
projectDAL
|
||||
});
|
||||
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
||||
const kmsService = kmsServiceFactory({
|
||||
kmsRootConfigDAL,
|
||||
keyStore,
|
||||
kmsDAL
|
||||
});
|
||||
|
||||
const trustedIpService = trustedIpServiceFactory({
|
||||
licenseService,
|
||||
projectDAL,
|
||||
@@ -823,6 +835,7 @@ export const registerRoutes = async (
|
||||
|
||||
await telemetryQueue.startTelemetryCheck();
|
||||
await dailyResourceCleanUp.startCleanUp();
|
||||
await kmsService.startService();
|
||||
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -906,7 +919,8 @@ export const registerRoutes = async (
|
||||
emailConfigured: z.boolean().optional(),
|
||||
inviteOnlySignup: z.boolean().optional(),
|
||||
redisConfigured: z.boolean().optional(),
|
||||
secretScanningConfigured: z.boolean().optional()
|
||||
secretScanningConfigured: z.boolean().optional(),
|
||||
samlDefaultOrgSlug: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -919,7 +933,8 @@ export const registerRoutes = async (
|
||||
emailConfigured: cfg.isSmtpConfigured,
|
||||
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
||||
redisConfigured: cfg.isRedisConfigured,
|
||||
secretScanningConfigured: cfg.isSecretScanningConfigured
|
||||
secretScanningConfigured: cfg.isSecretScanningConfigured,
|
||||
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list";
|
||||
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
|
||||
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -42,39 +42,11 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService),
|
||||
targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId),
|
||||
owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner),
|
||||
url: z.string().trim().optional().describe(INTEGRATION.CREATE.url),
|
||||
path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
|
||||
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
|
||||
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
|
||||
metadata: z
|
||||
.object({
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
mappingBehavior: z
|
||||
.nativeEnum(IntegrationMappingBehavior)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
labelValue: z.string()
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
|
||||
secretAWSTag: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
|
||||
})
|
||||
.default({})
|
||||
metadata: IntegrationMetadataSchema.default({})
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -160,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
||||
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
||||
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
|
||||
metadata: z
|
||||
.object({
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
labelValue: z.string()
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
|
||||
secretAWSTag: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
|
||||
})
|
||||
.optional()
|
||||
metadata: IntegrationMetadataSchema.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -30,7 +30,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
||||
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
|
||||
path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
|
||||
}),
|
||||
isReplication: z.boolean().default(false)
|
||||
isReplication: z.boolean().default(false).describe(SECRET_IMPORTS.CREATE.isReplication)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -80,7 +80,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
email: z.string().trim(),
|
||||
providerAuthToken: z.string().trim().optional(),
|
||||
clientProof: z.string().trim()
|
||||
clientProof: z.string().trim(),
|
||||
captchaToken: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.discriminatedUnion("mfaEnabled", [
|
||||
@@ -106,6 +107,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const data = await server.services.login.loginExchangeClientProof({
|
||||
captchaToken: req.body.captchaToken,
|
||||
email: req.body.email,
|
||||
ip: req.realIp,
|
||||
userAgent,
|
||||
|
||||
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
@@ -176,12 +177,16 @@ export const authLoginServiceFactory = ({
|
||||
clientProof,
|
||||
ip,
|
||||
userAgent,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
}: TLoginClientProofDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||
username: email
|
||||
});
|
||||
if (!userEnc) throw new Error("Failed to find user");
|
||||
const user = await userDAL.findById(userEnc.userId);
|
||||
const cfg = getConfig();
|
||||
|
||||
let authMethod = AuthMethod.EMAIL;
|
||||
@@ -196,6 +201,31 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
user.consecutiveFailedPasswordAttempts &&
|
||||
user.consecutiveFailedPasswordAttempts >= 10 &&
|
||||
Boolean(appCfg.CAPTCHA_SECRET)
|
||||
) {
|
||||
if (!captchaToken) {
|
||||
throw new BadRequestError({
|
||||
name: "Captcha Required",
|
||||
message: "Accomplish the required captcha by logging in via Web"
|
||||
});
|
||||
}
|
||||
|
||||
// validate captcha token
|
||||
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
|
||||
response: captchaToken,
|
||||
secret: appCfg.CAPTCHA_SECRET
|
||||
});
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new BadRequestError({
|
||||
name: "Invalid Captcha"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
||||
const isValidClientProof = await srpCheckClientProof(
|
||||
userEnc.salt,
|
||||
@@ -204,15 +234,31 @@ export const authLoginServiceFactory = ({
|
||||
userEnc.clientPublicKey,
|
||||
clientProof
|
||||
);
|
||||
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||
|
||||
if (!isValidClientProof) {
|
||||
await userDAL.update(
|
||||
{ id: userEnc.userId },
|
||||
{
|
||||
$incr: {
|
||||
consecutiveFailedPasswordAttempts: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
throw new Error("Failed to authenticate. Try again?");
|
||||
}
|
||||
|
||||
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||
serverPrivateKey: null,
|
||||
clientPublicKey: null
|
||||
});
|
||||
|
||||
await userDAL.updateById(userEnc.userId, {
|
||||
consecutiveFailedPasswordAttempts: 0
|
||||
});
|
||||
|
||||
// send multi factor auth token if they it enabled
|
||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||
const user = await userDAL.findById(userEnc.userId);
|
||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||
|
||||
const mfaToken = jwt.sign(
|
||||
|
||||
@@ -12,6 +12,7 @@ export type TLoginClientProofDTO = {
|
||||
providerAuthToken?: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
captchaToken?: string;
|
||||
};
|
||||
|
||||
export type TVerifyMfaTokenDTO = {
|
||||
|
||||
@@ -199,6 +199,7 @@ export const integrationAuthServiceFactory = ({
|
||||
projectId,
|
||||
namespace,
|
||||
integration,
|
||||
url,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8,
|
||||
...(integration === Integrations.GCP_SECRET_MANAGER
|
||||
|
||||
@@ -30,7 +30,8 @@ export enum Integrations {
|
||||
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
|
||||
CLOUD_66 = "cloud-66",
|
||||
NORTHFLANK = "northflank",
|
||||
HASURA_CLOUD = "hasura-cloud"
|
||||
HASURA_CLOUD = "hasura-cloud",
|
||||
RUNDECK = "rundeck"
|
||||
}
|
||||
|
||||
export enum IntegrationType {
|
||||
@@ -368,6 +369,15 @@ export const getIntegrationOptions = async () => {
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Rundeck",
|
||||
slug: "rundeck",
|
||||
image: "Rundeck.svg",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
@@ -1363,38 +1364,41 @@ const syncSecretsGitHub = async ({
|
||||
}
|
||||
}
|
||||
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (
|
||||
!(encryptedSecret.name in secrets) &&
|
||||
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
|
||||
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
|
||||
) {
|
||||
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,
|
||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||
if (metadata.shouldEnableDelete) {
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (
|
||||
!(encryptedSecret.name in secrets) &&
|
||||
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
|
||||
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
|
||||
) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1917,13 +1921,13 @@ const syncSecretsGitLab = async ({
|
||||
return allEnvVariables;
|
||||
};
|
||||
|
||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
|
||||
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
||||
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
||||
.filter((gitLabSecret) => {
|
||||
let isValid = true;
|
||||
|
||||
const metadata = z.record(z.any()).parse(integration.metadata);
|
||||
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
|
||||
isValid = false;
|
||||
}
|
||||
@@ -1943,8 +1947,8 @@ const syncSecretsGitLab = async ({
|
||||
{
|
||||
key,
|
||||
value: secrets[key].value,
|
||||
protected: false,
|
||||
masked: false,
|
||||
protected: Boolean(metadata.shouldProtectSecrets),
|
||||
masked: Boolean(metadata.shouldMaskSecrets),
|
||||
raw: false,
|
||||
environment_scope: integration.targetEnvironment
|
||||
},
|
||||
@@ -1961,7 +1965,9 @@ const syncSecretsGitLab = async ({
|
||||
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
||||
{
|
||||
...existingSecret,
|
||||
value: secrets[existingSecret.key].value
|
||||
value: secrets[existingSecret.key].value,
|
||||
protected: Boolean(metadata.shouldProtectSecrets),
|
||||
masked: Boolean(metadata.shouldMaskSecrets)
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -2750,6 +2756,20 @@ const syncSecretsCloudflarePages = async ({
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const metadata = z.record(z.any()).parse(integration.metadata);
|
||||
if (metadata.shouldAutoRedeploy) {
|
||||
await request.post(
|
||||
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}/deployments`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3355,6 +3375,82 @@ const syncSecretsHasuraCloud = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/** Sync/push [secrets] to Rundeck
|
||||
* @param {Object} obj
|
||||
* @param {TIntegrations} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessToken - access token for Rundeck integration
|
||||
*/
|
||||
const syncSecretsRundeck = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
interface RundeckSecretResource {
|
||||
name: string;
|
||||
}
|
||||
interface RundeckSecretsGetRes {
|
||||
resources: RundeckSecretResource[];
|
||||
}
|
||||
|
||||
let existingRundeckSecrets: string[] = [];
|
||||
|
||||
try {
|
||||
const listResult = await request.get<RundeckSecretsGetRes>(
|
||||
`${integration.url}/api/44/storage/${integration.path}`,
|
||||
{
|
||||
headers: {
|
||||
"X-Rundeck-Auth-Token": accessToken
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
existingRundeckSecrets = listResult.data.resources.map((res) => res.name);
|
||||
} catch (err) {
|
||||
logger.info("No existing rundeck secrets");
|
||||
}
|
||||
|
||||
try {
|
||||
for await (const [key, value] of Object.entries(secrets)) {
|
||||
if (existingRundeckSecrets.includes(key)) {
|
||||
await request.put(`${integration.url}/api/44/storage/${integration.path}/${key}`, value.value, {
|
||||
headers: {
|
||||
"X-Rundeck-Auth-Token": accessToken,
|
||||
"Content-Type": "application/x-rundeck-data-password"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await request.post(`${integration.url}/api/44/storage/${integration.path}/${key}`, value.value, {
|
||||
headers: {
|
||||
"X-Rundeck-Auth-Token": accessToken,
|
||||
"Content-Type": "application/x-rundeck-data-password"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const existingSecret of existingRundeckSecrets) {
|
||||
if (!(existingSecret in secrets)) {
|
||||
await request.delete(`${integration.url}/api/44/storage/${integration.path}/${existingSecret}`, {
|
||||
headers: {
|
||||
"X-Rundeck-Auth-Token": accessToken
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
throw new Error(
|
||||
`Ensure that the provided Rundeck URL is accessible by Infisical and that the linked API token has sufficient permissions.\n\n${
|
||||
(err as Error).message
|
||||
}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
*
|
||||
@@ -3621,6 +3717,13 @@ export const syncIntegrationSecrets = async ({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case Integrations.RUNDECK:
|
||||
await syncSecretsRundeck({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { INTEGRATION } from "@app/lib/api-docs";
|
||||
|
||||
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
|
||||
|
||||
export const IntegrationMetadataSchema = z.object({
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
mappingBehavior: z
|
||||
.nativeEnum(IntegrationMappingBehavior)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
labelValue: z.string()
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
|
||||
secretAWSTag: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
|
||||
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
|
||||
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
|
||||
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
|
||||
});
|
||||
@@ -43,6 +43,7 @@ export const integrationServiceFactory = ({
|
||||
scope,
|
||||
actorId,
|
||||
region,
|
||||
url,
|
||||
isActive,
|
||||
metadata,
|
||||
secretPath,
|
||||
@@ -87,6 +88,7 @@ export const integrationServiceFactory = ({
|
||||
region,
|
||||
scope,
|
||||
owner,
|
||||
url,
|
||||
appId,
|
||||
path,
|
||||
app,
|
||||
|
||||
@@ -12,6 +12,7 @@ export type TCreateIntegrationDTO = {
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
owner?: string;
|
||||
url?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
scope?: string;
|
||||
@@ -28,6 +29,9 @@ export type TCreateIntegrationDTO = {
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldMaskSecrets?: boolean;
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -53,6 +57,7 @@ export type TUpdateIntegrationDTO = {
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TKmsDALFactory = ReturnType<typeof kmsDALFactory>;
|
||||
|
||||
export const kmsDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsKey);
|
||||
return kmsOrm;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
||||
|
||||
export const kmsRootConfigDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsServerRootConfig);
|
||||
return kmsOrm;
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { randomSecureBytes } from "@app/lib/crypto";
|
||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TKmsDALFactory } from "./kms-dal";
|
||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||
import { TDecryptWithKmsDTO, TEncryptWithKmsDTO, TGenerateKMSDTO } from "./kms-types";
|
||||
|
||||
type TKmsServiceFactoryDep = {
|
||||
kmsDAL: TKmsDALFactory;
|
||||
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
||||
};
|
||||
|
||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
||||
|
||||
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
|
||||
const KMS_ROOT_CREATION_WAIT_TIME = 10;
|
||||
|
||||
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
|
||||
const KMS_VERSION = "v01";
|
||||
const KMS_VERSION_BLOB_LENGTH = 3;
|
||||
export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsServiceFactoryDep) => {
|
||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||
|
||||
// this is used symmetric encryption
|
||||
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true }: TGenerateKMSDTO) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKeyMaterial = randomSecureBytes(32);
|
||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
const { encryptedKey, ...doc } = await kmsDAL.create({
|
||||
version: 1,
|
||||
encryptedKey: encryptedKeyMaterial,
|
||||
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
||||
isReserved,
|
||||
orgId: scopeType === "org" ? scopeId : undefined,
|
||||
projectId: scopeType === "project" ? scopeId : undefined
|
||||
});
|
||||
return doc;
|
||||
};
|
||||
|
||||
const encrypt = async ({ kmsId, plainText }: TEncryptWithKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
|
||||
|
||||
// Buffer#1 encrypted text + Buffer#2 version number
|
||||
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
|
||||
return { cipherTextBlob };
|
||||
};
|
||||
|
||||
const decrypt = async ({ cipherTextBlob: versionedCipherTextBlob, kmsId }: TDecryptWithKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
|
||||
return decryptedBlob;
|
||||
};
|
||||
|
||||
const startService = async () => {
|
||||
const appCfg = getConfig();
|
||||
// This will switch to a seal process and HMS flow in future
|
||||
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
|
||||
// if root key its base64 encoded
|
||||
const isBase64 = !appCfg.ENCRYPTION_KEY;
|
||||
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
|
||||
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
|
||||
|
||||
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
|
||||
if (!lock) {
|
||||
await keyStore.waitTillReady({
|
||||
key: KMS_ROOT_CREATION_WAIT_KEY,
|
||||
keyCheckCb: (val) => val === "true",
|
||||
waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key")
|
||||
});
|
||||
}
|
||||
|
||||
// check if KMS root key was already generated and saved in DB
|
||||
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
if (kmsRootConfig) {
|
||||
if (lock) await lock.release();
|
||||
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
|
||||
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||
// set the flag so that other instancen nodes can start
|
||||
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("KMS: Generating ROOT Key");
|
||||
const newRootKey = randomSecureBytes(32);
|
||||
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
|
||||
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID });
|
||||
|
||||
// set the flag so that other instancen nodes can start
|
||||
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||
logger.info("KMS: Saved and loaded ROOT Key into memory");
|
||||
if (lock) await lock.release();
|
||||
ROOT_ENCRYPTION_KEY = newRootKey;
|
||||
};
|
||||
|
||||
return {
|
||||
startService,
|
||||
generateKmsKey,
|
||||
encrypt,
|
||||
decrypt
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export type TGenerateKMSDTO = {
|
||||
scopeType: "project" | "org";
|
||||
scopeId: string;
|
||||
isReserved?: boolean;
|
||||
};
|
||||
|
||||
export type TEncryptWithKmsDTO = {
|
||||
kmsId: string;
|
||||
plainText: Buffer;
|
||||
};
|
||||
|
||||
export type TDecryptWithKmsDTO = {
|
||||
kmsId: string;
|
||||
cipherTextBlob: Buffer;
|
||||
};
|
||||
@@ -315,6 +315,7 @@ export const orgServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await licenseService.updateSubscriptionOrgMemberCount(org.id);
|
||||
await orgBotDAL.create(
|
||||
{
|
||||
name: org.name,
|
||||
|
||||
@@ -309,7 +309,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
||||
};
|
||||
|
||||
const expandSecrets = async (
|
||||
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
|
||||
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
) => {
|
||||
const expandedSec: Record<string, string> = {};
|
||||
const interpolatedSec: Record<string, string> = {};
|
||||
@@ -329,8 +329,8 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
||||
// should not do multi line encoding if user has set it to skip
|
||||
// eslint-disable-next-line
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? expandedSec[key]
|
||||
: formatMultiValueEnv(expandedSec[key]);
|
||||
? formatMultiValueEnv(expandedSec[key])
|
||||
: expandedSec[key];
|
||||
// eslint-disable-next-line
|
||||
continue;
|
||||
}
|
||||
@@ -347,7 +347,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal;
|
||||
}
|
||||
|
||||
return secrets;
|
||||
@@ -395,7 +395,8 @@ export const decryptSecretRaw = (
|
||||
type: secret.type,
|
||||
_id: secret.id,
|
||||
id: secret.id,
|
||||
user: secret.userId
|
||||
user: secret.userId,
|
||||
skipMultilineEncoding: secret.skipMultilineEncoding
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||
@@ -67,7 +69,10 @@ const MAX_SYNC_SECRET_DEPTH = 5;
|
||||
export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
|
||||
`secret-queue-dedupe-${environment}-${secretPath}`;
|
||||
|
||||
type TIntegrationSecret = Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>;
|
||||
type TIntegrationSecret = Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
|
||||
>;
|
||||
export const secretQueueFactory = ({
|
||||
queueService,
|
||||
integrationDAL,
|
||||
@@ -567,11 +572,14 @@ export const secretQueueFactory = ({
|
||||
isSynced: true
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
logger.info("Secret integration sync error:", err);
|
||||
logger.info("Secret integration sync error: %o", err);
|
||||
const message =
|
||||
err instanceof AxiosError ? JSON.stringify((err as AxiosError)?.response?.data) : (err as Error)?.message;
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastSyncJobId: job.id,
|
||||
lastUsed: new Date(),
|
||||
syncMessage: (err as Error)?.message,
|
||||
syncMessage: message,
|
||||
isSynced: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -952,15 +952,49 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
|
||||
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
|
||||
const decryptedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
|
||||
...el,
|
||||
secrets: importedSecrets.map((sec) =>
|
||||
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
|
||||
const decryptedImportSecrets = importedSecrets.map((sec) =>
|
||||
decryptSecretRaw(
|
||||
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
|
||||
botKey
|
||||
)
|
||||
)
|
||||
}));
|
||||
);
|
||||
|
||||
// secret-override to handle duplicate keys from different import levels
|
||||
// this prioritizes secret values from direct imports
|
||||
const importedKeys = new Set<string>();
|
||||
const importedEntries = decryptedImportSecrets.reduce(
|
||||
(
|
||||
accum: {
|
||||
secretKey: string;
|
||||
secretPath: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
secretValue: string;
|
||||
secretComment: string;
|
||||
version: number;
|
||||
type: string;
|
||||
_id: string;
|
||||
id: string;
|
||||
user: string | null | undefined;
|
||||
skipMultilineEncoding: boolean | null | undefined;
|
||||
}[],
|
||||
sec
|
||||
) => {
|
||||
if (!importedKeys.has(sec.secretKey)) {
|
||||
importedKeys.add(sec.secretKey);
|
||||
return [...accum, sec];
|
||||
}
|
||||
return accum;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
...el,
|
||||
secrets: importedEntries
|
||||
};
|
||||
});
|
||||
|
||||
if (expandSecretReferences) {
|
||||
const expandSecrets = interpolateSecrets({
|
||||
@@ -971,10 +1005,24 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
|
||||
const batchSecretsExpand = async (
|
||||
secretBatch: { secretKey: string; secretValue: string; secretComment?: string; secretPath: string }[]
|
||||
secretBatch: {
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretComment?: string;
|
||||
secretPath: string;
|
||||
skipMultilineEncoding: boolean | null | undefined;
|
||||
}[]
|
||||
) => {
|
||||
// Group secrets by secretPath
|
||||
const secretsByPath: Record<string, { secretKey: string; secretValue: string; secretComment?: string }[]> = {};
|
||||
const secretsByPath: Record<
|
||||
string,
|
||||
{
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding: boolean | null | undefined;
|
||||
}[]
|
||||
> = {};
|
||||
|
||||
secretBatch.forEach((secret) => {
|
||||
if (!secretsByPath[secret.secretPath]) {
|
||||
@@ -990,11 +1038,15 @@ export const secretServiceFactory = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
const secretRecord: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
|
||||
const secretRecord: Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
|
||||
> = {};
|
||||
secretsByPath[secPath].forEach((decryptedSecret) => {
|
||||
secretRecord[decryptedSecret.secretKey] = {
|
||||
value: decryptedSecret.secretValue,
|
||||
comment: decryptedSecret.secretComment
|
||||
comment: decryptedSecret.secretComment,
|
||||
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1011,12 +1063,12 @@ export const secretServiceFactory = ({
|
||||
await batchSecretsExpand(decryptedSecrets);
|
||||
|
||||
// expand imports by batch
|
||||
await Promise.all(decryptedImports.map((decryptedImport) => batchSecretsExpand(decryptedImport.secrets)));
|
||||
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
|
||||
}
|
||||
|
||||
return {
|
||||
secrets: decryptedSecrets,
|
||||
imports: decryptedImports
|
||||
imports: processedImports
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -41,21 +41,8 @@ export enum SmtpHost {
|
||||
Office365 = "smtp.office365.com"
|
||||
}
|
||||
|
||||
export const getTlsOption = (host?: SmtpHost | string, secure?: boolean) => {
|
||||
if (!secure) return { secure: false };
|
||||
if (!host) return { secure: true };
|
||||
|
||||
if ((host as SmtpHost) === SmtpHost.Sendgrid) {
|
||||
return { secure: true, port: 465 }; // more details here https://nodemailer.com/smtp/
|
||||
}
|
||||
if (host.includes("amazonaws.com")) {
|
||||
return { tls: { ciphers: "TLSv1.2" } };
|
||||
}
|
||||
return { requireTLS: true, tls: { ciphers: "TLSv1.2" } };
|
||||
};
|
||||
|
||||
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||
const smtp = createTransport({ ...cfg, ...getTlsOption(cfg.host, cfg.secure) });
|
||||
const smtp = createTransport(cfg);
|
||||
const isSmtpOn = Boolean(cfg.host);
|
||||
|
||||
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
||||
|
||||
Reference in New Issue
Block a user