improvements: address feedback

This commit is contained in:
Scott Wilson
2025-04-02 14:11:59 -07:00
parent 766c1242fd
commit 577c81be65
44 changed files with 453 additions and 269 deletions
@@ -1,29 +0,0 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
const INDEX_NAME = "idx_unique_secret_v2_key";
export async function up(knex: Knex): Promise<void> {
const hasKeyCol = await knex.schema.hasColumn(TableName.SecretV2, "key");
const hasFolderIdCol = await knex.schema.hasColumn(TableName.SecretV2, "folderId");
const hasTypeCol = await knex.schema.hasColumn(TableName.SecretV2, "type");
if (hasKeyCol && hasFolderIdCol && hasTypeCol) {
await knex.raw(`
CREATE UNIQUE INDEX ${INDEX_NAME}
ON ${TableName.SecretV2} ("key", "folderId")
WHERE type = 'shared'
`);
}
}
export async function down(knex: Knex): Promise<void> {
const hasKeyCol = await knex.schema.hasColumn(TableName.SecretV2, "key");
const hasFolderIdCol = await knex.schema.hasColumn(TableName.SecretV2, "folderId");
const hasTypeCol = await knex.schema.hasColumn(TableName.SecretV2, "type");
if (hasKeyCol && hasFolderIdCol && hasTypeCol) {
await knex.raw(`DROP INDEX IF EXISTS ${INDEX_NAME}`);
}
}
@@ -277,8 +277,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(), reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
secretPath: z.string(), secretPath: z.string(),
commits: secretRawSchema commits: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
.extend({ .extend({
secretValue: z.string().optional(),
isRotatedSecret: z.boolean().optional(),
op: z.string(), op: z.string(),
tags: SanitizedTagSchema.array().optional(), tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish(), secretMetadata: ResourceMetadataSchema.nullish(),
+2 -1
View File
@@ -33,7 +33,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretId: z.string(), secretId: z.string(),
tags: SanitizedTagSchema.array() tags: SanitizedTagSchema.array(),
isRotatedSecret: z.boolean().optional()
}) })
.array(), .array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
@@ -151,7 +151,7 @@ export const registerSecretRotationEndpoints = <
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
description: `Get the specified ${rotationType} Rotation by name and project ID.`, description: `Get the specified ${rotationType} Rotation by name, secret path, environment and project ID.`,
params: z.object({ params: z.object({
rotationName: z rotationName: z
.string() .string()
@@ -13,7 +13,8 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat( const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat(
(appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)), (appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)),
getDbConnectionHost(appCfg.REDIS_URL) getDbConnectionHost(appCfg.REDIS_URL),
getDbConnectionHost(appCfg.AUDIT_LOGS_DB_CONNECTION_URI)
); );
// get host db ip // get host db ip
@@ -257,6 +257,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema("secVerTag") db.ref("id").withSchema("secVerTag")
) )
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretV2}.id`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2)) .select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({ .select({
secVerTagId: "secVerTag.id", secVerTagId: "secVerTag.id",
@@ -285,7 +290,8 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
); )
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
const formatedDoc = sqlNestRelationships({ const formatedDoc = sqlNestRelationships({
data: doc, data: doc,
key: "id", key: "id",
@@ -304,14 +310,16 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "secretId", key: "secretId",
label: "secret" as const, label: "secret" as const,
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) => mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId, rotationId }) =>
secretId secretId
? { ? {
id: secretId, id: secretId,
version: orgSecVersion, version: orgSecVersion,
key: orgSecKey, key: orgSecKey,
encryptedValue: orgSecValue, encryptedValue: orgSecValue,
encryptedComment: orgSecComment encryptedComment: orgSecComment,
isRotatedSecret: Boolean(rotationId),
rotationId
} }
: undefined : undefined
}, },
@@ -262,7 +262,13 @@ export const secretApprovalRequestServiceFactory = ({
id: el.id, id: el.id,
version: el.version, version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO, secretMetadata: el.secretMetadata as ResourceMetadataDTO,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", isRotatedSecret: el.secret.isRotatedSecret,
// eslint-disable-next-line no-nested-ternary
secretValue: el.secret.isRotatedSecret
? undefined
: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "",
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: "", : "",
@@ -609,7 +615,7 @@ export const secretApprovalRequestServiceFactory = ({
tx, tx,
inputSecrets: secretUpdationCommits.map((el) => { inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue = const encryptedValue =
typeof el.encryptedValue !== "undefined" !el.secret.isRotatedSecret && typeof el.encryptedValue !== "undefined"
? { ? {
encryptedValue: el.encryptedValue as Buffer, encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue references: el.encryptedValue
@@ -189,9 +189,11 @@ export const secretRotationV2DALFactory = (
.countDistinct(`${TableName.SecretRotationV2}.name`); .countDistinct(`${TableName.SecretRotationV2}.name`);
if (search) { if (search) {
void query void query.where((qb) => {
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`) void qb
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`); .whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
});
} }
const result = await query; const result = await query;
@@ -23,7 +23,7 @@ export const listSecretRotationOptions = () => {
return Object.values(SECRET_ROTATION_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); return Object.values(SECRET_ROTATION_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
}; };
const getNextUTCMidnight = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => { const getNextUTCDayInterval = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => {
const now = new Date(); const now = new Date();
return new Date( return new Date(
@@ -39,7 +39,7 @@ const getNextUTCMidnight = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]
); );
}; };
const getNextUTCMinute = ({ minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => { const getNextUTCMinuteInterval = ({ minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => {
const now = new Date(); const now = new Date();
return new Date( return new Date(
Date.UTC( Date.UTC(
@@ -58,10 +58,10 @@ export const getNextUtcRotationInterval = (rotateAtUtc?: TSecretRotationV2["rota
const appCfg = getConfig(); const appCfg = getConfig();
if (appCfg.isRotationDevelopmentMode) { if (appCfg.isRotationDevelopmentMode) {
return getNextUTCMinute(rotateAtUtc); return getNextUTCMinuteInterval(rotateAtUtc);
} }
return getNextUTCMidnight(rotateAtUtc); return getNextUTCDayInterval(rotateAtUtc);
}; };
export const encryptSecretRotationCredentials = async ({ export const encryptSecretRotationCredentials = async ({
@@ -80,7 +80,7 @@ export const secretRotationV2QueueServiceFactory = async ({
{ {
batchSize: 1, batchSize: 1,
workerCount: 1, workerCount: 1,
pollingIntervalSeconds: 0.5 pollingIntervalSeconds: appCfg.isRotationDevelopmentMode ? 0.5 : 30
} }
); );
@@ -122,7 +122,7 @@ export const secretRotationV2QueueServiceFactory = async ({
}, },
{ {
batchSize: 1, batchSize: 1,
workerCount: 30, workerCount: 2,
pollingIntervalSeconds: 0.5 pollingIntervalSeconds: 0.5
} }
); );
@@ -179,8 +179,8 @@ export const secretRotationV2QueueServiceFactory = async ({
}, },
{ {
batchSize: 1, batchSize: 1,
workerCount: 5, workerCount: 2,
pollingIntervalSeconds: 30 pollingIntervalSeconds: 1
} }
); );
@@ -1,4 +1,5 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { Knex } from "knex";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { ActionProjectType, SecretType, TableName } from "@app/db/schemas"; import { ActionProjectType, SecretType, TableName } from "@app/db/schemas";
@@ -46,7 +47,7 @@ import {
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { sqlCredentialsRotationFactory } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials"; import { sqlCredentialsRotationFactory } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { DatabaseErrorCode } from "@app/lib/error-codes"; import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors";
@@ -141,6 +142,37 @@ export const secretRotationV2ServiceFactory = ({
); );
}; };
const $throwOnConflictingSecrets = async ({
secretKeys,
folderId,
tx,
secretPath
}: {
secretKeys: string[];
folderId: string;
tx: Knex;
secretPath: string;
}) => {
const conflictingSecrets = await secretV2BridgeDAL.find(
{
$in: {
[`${TableName.SecretV2}.key` as "key"]: secretKeys
},
[`${TableName.SecretV2}.folderId` as "folderId"]: folderId,
[`${TableName.SecretV2}.type` as "type"]: SecretType.Shared
},
{ tx }
);
if (conflictingSecrets.length) {
throw new BadRequestError({
message: `The following secrets already exist at the path "${secretPath}": ${conflictingSecrets
.map(({ key }) => key)
.join(", ")}`
});
}
};
const listSecretRotationsByProjectId = async ( const listSecretRotationsByProjectId = async (
{ projectId, type }: TListSecretRotationsV2ByProjectId, { projectId, type }: TListSecretRotationsV2ByProjectId,
actor: OrgServiceActor actor: OrgServiceActor
@@ -345,6 +377,7 @@ export const secretRotationV2ServiceFactory = ({
secretPath, secretPath,
environment, environment,
rotateAtUtc = { hours: 0, minutes: 0 }, rotateAtUtc = { hours: 0, minutes: 0 },
secretsMapping,
...payload ...payload
}: TCreateSecretRotationV2DTO, }: TCreateSecretRotationV2DTO,
actor: OrgServiceActor actor: OrgServiceActor
@@ -368,7 +401,10 @@ export const secretRotationV2ServiceFactory = ({
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge) if (!shouldUseSecretV2Bridge)
throw new BadRequestError({ message: "Project version does not support Secret Rotation V2" }); throw new BadRequestError({
message:
"Project version does not support Secret Rotation V2. Please upgrade your project via the Infiscal Dashboard to gain access."
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Create, ProjectPermissionSecretRotationActions.Create,
@@ -389,7 +425,7 @@ export const secretRotationV2ServiceFactory = ({
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({ const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({
parameters: payload.parameters, parameters: payload.parameters,
secretsMapping: payload.secretsMapping, secretsMapping,
connection connection
} as TSecretRotationV2WithConnection); } as TSecretRotationV2WithConnection);
@@ -405,9 +441,19 @@ export const secretRotationV2ServiceFactory = ({
}); });
return secretRotationV2DAL.transaction(async (tx) => { return secretRotationV2DAL.transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SecretRotationV2Creation(folder.id)]);
await $throwOnConflictingSecrets({
secretPath,
secretKeys: Object.values(secretsMapping),
tx,
folderId: folder.id
});
const createdRotation = await secretRotationV2DAL.create( const createdRotation = await secretRotationV2DAL.create(
{ {
folderId: folder.id, folderId: folder.id,
secretsMapping,
...payload, ...payload,
encryptedGeneratedCredentials, encryptedGeneratedCredentials,
rotateAtUtc, rotateAtUtc,
@@ -483,12 +529,6 @@ export const secretRotationV2ServiceFactory = ({
throw new BadRequestError({ throw new BadRequestError({
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"` message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"`
}); });
case TableName.SecretV2:
throw new BadRequestError({
message: `One or more of the following secrets already exists at the secret path "${secretPath}": ${Object.values(
payload.secretsMapping
).join(", ")}`
});
default: default:
throw err; throw err;
} }
@@ -497,6 +537,8 @@ export const secretRotationV2ServiceFactory = ({
throw err; throw err;
} }
if (err instanceof BadRequestError) throw err;
throw new BadRequestError({ throw new BadRequestError({
message: parseRotationErrorMessage(err) message: parseRotationErrorMessage(err)
}); });
@@ -521,7 +563,8 @@ export const secretRotationV2ServiceFactory = ({
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}` message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}`
}); });
const { folder, environment, projectId, folderId, connection, secretsMapping } = secretRotation; const { folder, environment, projectId, folderId, connection } = secretRotation;
const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"];
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
@@ -551,26 +594,36 @@ export const secretRotationV2ServiceFactory = ({
isManualRotation: false isManualRotation: false
}); });
let secretsMappingUpdated = false;
try { try {
const updatedSecretRotation = await secretRotationV2DAL.transaction(async (tx) => { const updatedSecretRotation = await secretRotationV2DAL.transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SecretRotationV2Creation(folder.id)]);
if (payload.secretsMapping && !isEqual(payload.secretsMapping, secretsMapping)) { if (payload.secretsMapping && !isEqual(payload.secretsMapping, secretsMapping)) {
const currentMappingKeys = Object.values(secretsMapping);
await $throwOnConflictingSecrets({
secretPath: folder.path,
secretKeys: Object.values(payload.secretsMapping).filter((key) => !currentMappingKeys.includes(key)),
tx,
folderId: folder.id
});
// update mapped secrets names // update mapped secrets names
await fnSecretBulkUpdate({ await fnSecretBulkUpdate({
folderId, folderId,
orgId: connection.orgId, orgId: connection.orgId,
tx, tx,
inputSecrets: Object.entries(secretsMapping as TSecretRotationV2["secretsMapping"]).map( inputSecrets: Object.entries(secretsMapping).map(([mappingKey, secretKey]) => ({
([mappingKey, secretKey]) => ({ filter: {
filter: { key: secretKey,
key: secretKey, folderId,
folderId, type: SecretType.Shared
type: SecretType.Shared },
}, data: {
data: { key: payload.secretsMapping![mappingKey as keyof TSecretRotationV2["secretsMapping"]]
key: payload.secretsMapping![mappingKey as keyof TSecretRotationV2["secretsMapping"]] }
} })),
})
),
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL, secretVersionTagDAL: secretVersionTagV2BridgeDAL,
@@ -578,14 +631,7 @@ export const secretRotationV2ServiceFactory = ({
resourceMetadataDAL resourceMetadataDAL
}); });
await snapshotService.performSnapshot(folder.id); secretsMappingUpdated = true;
await secretQueueService.syncSecrets({
orgId: connection.orgId,
secretPath: folder.path,
projectId,
environmentSlug: environment.slug,
excludeReplication: true
});
} }
return secretRotationV2DAL.updateById( return secretRotationV2DAL.updateById(
@@ -598,6 +644,17 @@ export const secretRotationV2ServiceFactory = ({
); );
}); });
if (secretsMappingUpdated) {
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({
orgId: connection.orgId,
secretPath: folder.path,
projectId,
environmentSlug: environment.slug,
excludeReplication: true
});
}
// queue for rotation if adjusted time falls before next cron // queue for rotation if adjusted time falls before next cron
if (nextRotationAt && nextRotationAt.getTime() < getNextUtcRotationInterval().getTime()) { if (nextRotationAt && nextRotationAt.getTime() < getNextUtcRotationInterval().getTime()) {
await queueService.queuePg( await queueService.queuePg(
@@ -620,20 +677,14 @@ export const secretRotationV2ServiceFactory = ({
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${folder.path}"` message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${folder.path}"`
}); });
break; break;
case TableName.SecretV2:
if (payload.secretsMapping)
throw new BadRequestError({
message: `One or more of the following secrets already exists at the secret path "${
folder.path
}": ${Object.values(payload.secretsMapping).join(", ")}`
});
break;
default: default:
throw err; throw err;
} }
} }
} }
if (err instanceof BadRequestError) throw err;
throw err; throw err;
} }
}; };
@@ -695,15 +746,6 @@ export const secretRotationV2ServiceFactory = ({
actorId: actor.id, // not actually used since rotated secrets are shared actorId: actor.id, // not actually used since rotated secrets are shared
tx tx
}); });
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({
orgId: connection.orgId,
secretPath: folder.path,
projectId,
environmentSlug: environment.slug,
excludeReplication: true
});
} }
return secretRotationV2DAL.deleteById(rotationId, tx); return secretRotationV2DAL.deleteById(rotationId, tx);
@@ -728,6 +770,17 @@ export const secretRotationV2ServiceFactory = ({
await deleteTransaction; await deleteTransaction;
} }
if (deleteSecrets) {
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({
orgId: connection.orgId,
secretPath: folder.path,
projectId,
environmentSlug: environment.slug,
excludeReplication: true
});
}
return expandSecretRotation(secretRotation, kmsService); return expandSecretRotation(secretRotation, kmsService);
}; };
@@ -398,8 +398,32 @@ export const secretSnapshotServiceFactory = ({
if (shouldUseBridge) { if (shouldUseBridge) {
const rollback = await snapshotDAL.transaction(async (tx) => { const rollback = await snapshotDAL.transaction(async (tx) => {
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx); const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
// this will remove all secrets in current folder const secretRotationIds = rollbackSnaps
const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx); .flatMap((snap) => snap.secretVersions)
.filter((el) => el.isRotatedSecret)
.map((el) => el.secretId);
// this will remove all secrets in current folder except rotated secrets which we ignore
const deletedTopLevelSecs = await secretV2BridgeDAL.delete(
{
$complex: {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: snapshot.folderId
},
{
operator: "notIn",
field: "id",
value: secretRotationIds
}
]
}
},
tx
);
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id); const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
// this will remove all secrets and folders on child // this will remove all secrets and folders on child
// due to sql foreign key and link list connection removing the folders removes everything below too // due to sql foreign key and link list connection removing the folders removes everything below too
@@ -424,28 +448,31 @@ export const secretSnapshotServiceFactory = ({
); );
const secrets = await secretV2BridgeDAL.insertMany( const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) => rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map( secretVersions
({ .filter((v) => !v.isRotatedSecret)
latestSecretVersion, .map(
version, ({
updatedAt, latestSecretVersion,
createdAt, version,
secretId, updatedAt,
envId, createdAt,
id, secretId,
tags, envId,
// exclude the bottom fields from the secret - they are for versioning only. id,
userActorId, tags,
identityActorId, // exclude the bottom fields from the secret - they are for versioning only.
actorType, userActorId,
...el identityActorId,
}) => ({ actorType,
...el, isRotatedSecret,
id: secretId, ...el
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion, }) => ({
folderId ...el,
}) id: secretId,
) version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
folderId
})
)
), ),
tx tx
); );
@@ -181,6 +181,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
`${TableName.SnapshotFolder}.folderVersionId`, `${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id` `${TableName.SecretFolderVersion}.id`
) )
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretRotationV2SecretMapping}.secretId`,
`${TableName.SecretVersionV2}.secretId`
)
.select(selectAllTableCols(TableName.SecretVersionV2)) .select(selectAllTableCols(TableName.SecretVersionV2))
.select( .select(
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"), db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
@@ -195,7 +200,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)
); );
return sqlNestRelationships({ return sqlNestRelationships({
data, data,
@@ -221,7 +227,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "id", key: "id",
label: "secretVersions" as const, label: "secretVersions" as const,
mapper: (el) => SecretVersionsV2Schema.parse(el), mapper: (el) => ({
...SecretVersionsV2Schema.parse(el),
isRotatedSecret: Boolean(el.rotationId),
rotationId: el.rotationId
}),
childrenMapper: [ childrenMapper: [
{ {
key: "tagVersionId", key: "tagVersionId",
@@ -476,6 +486,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`, `${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id` `${TableName.SecretTag}.id`
) )
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretVersionV2}.secretId`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.leftJoin<{ latestSecretVersion: number }>( .leftJoin<{ latestSecretVersion: number }>(
(tx || db)(TableName.SecretVersionV2) (tx || db)(TableName.SecretVersionV2)
.groupBy("secretId") .groupBy("secretId")
@@ -506,7 +521,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)
); );
const formated = sqlNestRelationships({ const formated = sqlNestRelationships({
@@ -523,7 +539,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
label: "secretVersions" as const, label: "secretVersions" as const,
mapper: (el) => ({ mapper: (el) => ({
...SecretVersionsV2Schema.parse(el), ...SecretVersionsV2Schema.parse(el),
latestSecretVersion: el.latestSecretVersion as number latestSecretVersion: el.latestSecretVersion as number,
isRotatedSecret: Boolean(el.rotationId)
}), }),
childrenMapper: [ childrenMapper: [
{ {
+2 -1
View File
@@ -8,7 +8,8 @@ export const PgSqlLock = {
SuperAdminInit: 2024, SuperAdminInit: 2024,
KmsRootKeyInit: 2025, KmsRootKeyInit: 2025,
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`), OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`) OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`)
} as const; } as const;
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>; export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
+3 -1
View File
@@ -1,6 +1,8 @@
import { URL } from "url"; // Import the URL class import { URL } from "url"; // Import the URL class
export const getDbConnectionHost = (urlString: string) => { export const getDbConnectionHost = (urlString?: string) => {
if (!urlString) return null;
try { try {
const url = new URL(urlString); const url = new URL(urlString);
// Split hostname and port (if provided) // Split hostname and port (if provided)
+15 -5
View File
@@ -2,11 +2,17 @@ import { Knex } from "knex";
import { UnauthorizedError } from "../errors"; import { UnauthorizedError } from "../errors";
type TKnexDynamicPrimitiveOperator<T extends object> = { type TKnexDynamicPrimitiveOperator<T extends object> =
operator: "eq" | "ne" | "startsWith" | "endsWith"; | {
value: string; operator: "eq" | "ne" | "startsWith" | "endsWith";
field: Extract<keyof T, string>; value: string;
}; field: Extract<keyof T, string>;
}
| {
operator: "notIn";
value: string[];
field: Extract<keyof T, string>;
};
type TKnexDynamicInOperator<T extends object> = { type TKnexDynamicInOperator<T extends object> = {
operator: "in"; operator: "in";
@@ -48,6 +54,10 @@ export const buildDynamicKnexQuery = <T extends object>(
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`); void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
break; break;
} }
case "notIn": {
void queryBuilder.whereNotIn(filterAst.field, filterAst.value);
break;
}
case "and": { case "and": {
filterAst.value.forEach((el) => { filterAst.value.forEach((el) => {
void queryBuilder.andWhere((subQueryBuilder) => { void queryBuilder.andWhere((subQueryBuilder) => {
@@ -25,7 +25,7 @@ import {
TAppConnectionRaw, TAppConnectionRaw,
TCreateAppConnectionDTO, TCreateAppConnectionDTO,
TUpdateAppConnectionDTO, TUpdateAppConnectionDTO,
TValidateAppConnectionCredentials TValidateAppConnectionCredentialsSchema
} from "./app-connection-types"; } from "./app-connection-types";
import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service"; import { awsConnectionService } from "./aws/aws-connection-service";
@@ -50,7 +50,7 @@ export type TAppConnectionServiceFactoryDep = {
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>; export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = { const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentialsSchema> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema, [AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema, [AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema, [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
@@ -170,26 +170,22 @@ export const appConnectionServiceFactory = ({
} as TAppConnectionConfig); } as TAppConnectionConfig);
try { try {
const createTransaction = (connectionCredentials: TAppConnection["credentials"]) => const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
appConnectionDAL.transaction(async (tx) => { const encryptedCredentials = await encryptAppConnectionCredentials({
const encryptedCredentials = await encryptAppConnectionCredentials({ credentials: connectionCredentials,
credentials: connectionCredentials, orgId: actor.orgId,
orgId: actor.orgId, kmsService
kmsService
});
return appConnectionDAL.create(
{
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
},
tx
);
}); });
return appConnectionDAL.create({
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
});
};
let connection: TAppConnectionRaw; let connection: TAppConnectionRaw;
if (params.isPlatformManagedCredentials) { if (params.isPlatformManagedCredentials) {
@@ -200,10 +196,10 @@ export const appConnectionServiceFactory = ({
credentials: validatedCredentials, credentials: validatedCredentials,
method method
} as TAppConnectionConfig, } as TAppConnectionConfig,
(platformCredentials) => createTransaction(platformCredentials) (platformCredentials) => createConnection(platformCredentials)
); );
} else { } else {
connection = await createTransaction(validatedCredentials); connection = await createConnection(validatedCredentials);
} }
return { return {
@@ -277,26 +273,21 @@ export const appConnectionServiceFactory = ({
} }
try { try {
const updateTransaction = (connectionCredentials: TAppConnection["credentials"] | undefined) => const updateConnection = async (connectionCredentials: TAppConnection["credentials"] | undefined) => {
appConnectionDAL.transaction(async (tx) => { const encryptedCredentials = connectionCredentials
const encryptedCredentials = connectionCredentials ? await encryptAppConnectionCredentials({
? await encryptAppConnectionCredentials({ credentials: connectionCredentials,
credentials: connectionCredentials,
orgId: actor.orgId,
kmsService
})
: undefined;
return appConnectionDAL.updateById(
connectionId,
{
orgId: actor.orgId, orgId: actor.orgId,
encryptedCredentials, kmsService
...params })
}, : undefined;
tx
); return appConnectionDAL.updateById(connectionId, {
orgId: actor.orgId,
encryptedCredentials,
...params
}); });
};
let updatedConnection: TAppConnectionRaw; let updatedConnection: TAppConnectionRaw;
@@ -312,10 +303,10 @@ export const appConnectionServiceFactory = ({
credentials: updatedCredentials, credentials: updatedCredentials,
method method
} as TAppConnectionConfig, } as TAppConnectionConfig,
(platformCredentials) => updateTransaction(platformCredentials) (platformCredentials) => updateConnection(platformCredentials)
); );
} else { } else {
updatedConnection = await updateTransaction(updatedCredentials); updatedConnection = await updateConnection(updatedCredentials);
} }
return await decryptAppConnection(updatedConnection, kmsService); return await decryptAppConnection(updatedConnection, kmsService);
@@ -3,40 +3,54 @@ import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sq
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { AWSRegion } from "./app-connection-enums"; import { AWSRegion } from "./app-connection-enums";
import { TAwsConnection, TAwsConnectionConfig, TAwsConnectionInput, TValidateAwsConnectionCredentials } from "./aws"; import {
TAwsConnection,
TAwsConnectionConfig,
TAwsConnectionInput,
TValidateAwsConnectionCredentialsSchema
} from "./aws";
import { import {
TAzureAppConfigurationConnection, TAzureAppConfigurationConnection,
TAzureAppConfigurationConnectionConfig, TAzureAppConfigurationConnectionConfig,
TAzureAppConfigurationConnectionInput, TAzureAppConfigurationConnectionInput,
TValidateAzureAppConfigurationConnectionCredentials TValidateAzureAppConfigurationConnectionCredentialsSchema
} from "./azure-app-configuration"; } from "./azure-app-configuration";
import { import {
TAzureKeyVaultConnection, TAzureKeyVaultConnection,
TAzureKeyVaultConnectionConfig, TAzureKeyVaultConnectionConfig,
TAzureKeyVaultConnectionInput, TAzureKeyVaultConnectionInput,
TValidateAzureKeyVaultConnectionCredentials TValidateAzureKeyVaultConnectionCredentialsSchema
} from "./azure-key-vault"; } from "./azure-key-vault";
import { import {
TDatabricksConnection, TDatabricksConnection,
TDatabricksConnectionConfig, TDatabricksConnectionConfig,
TDatabricksConnectionInput, TDatabricksConnectionInput,
TValidateDatabricksConnectionCredentials TValidateDatabricksConnectionCredentialsSchema
} from "./databricks"; } from "./databricks";
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp"; import {
TGcpConnection,
TGcpConnectionConfig,
TGcpConnectionInput,
TValidateGcpConnectionCredentialsSchema
} from "./gcp";
import { import {
TGitHubConnection, TGitHubConnection,
TGitHubConnectionConfig, TGitHubConnectionConfig,
TGitHubConnectionInput, TGitHubConnectionInput,
TValidateGitHubConnectionCredentials TValidateGitHubConnectionCredentialsSchema
} from "./github"; } from "./github";
import { import {
THumanitecConnection, THumanitecConnection,
THumanitecConnectionConfig, THumanitecConnectionConfig,
THumanitecConnectionInput, THumanitecConnectionInput,
TValidateHumanitecConnectionCredentials TValidateHumanitecConnectionCredentialsSchema
} from "./humanitec"; } from "./humanitec";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentials } from "./mssql"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { TPostgresConnection, TPostgresConnectionInput, TValidatePostgresConnectionCredentials } from "./postgres"; import {
TPostgresConnection,
TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema
} from "./postgres";
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
@@ -87,16 +101,16 @@ export type TAppConnectionConfig =
| THumanitecConnectionConfig | THumanitecConnectionConfig
| TSqlConnectionConfig; | TSqlConnectionConfig;
export type TValidateAppConnectionCredentials = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentials | TValidateAwsConnectionCredentialsSchema
| TValidateGitHubConnectionCredentials | TValidateGitHubConnectionCredentialsSchema
| TValidateGcpConnectionCredentials | TValidateGcpConnectionCredentialsSchema
| TValidateAzureKeyVaultConnectionCredentials | TValidateAzureKeyVaultConnectionCredentialsSchema
| TValidateAzureAppConfigurationConnectionCredentials | TValidateAzureAppConfigurationConnectionCredentialsSchema
| TValidateDatabricksConnectionCredentials | TValidateDatabricksConnectionCredentialsSchema
| TValidateHumanitecConnectionCredentials | TValidateHumanitecConnectionCredentialsSchema
| TValidatePostgresConnectionCredentials | TValidatePostgresConnectionCredentialsSchema
| TValidateMsSqlConnectionCredentials; | TValidateMsSqlConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -15,7 +15,7 @@ export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
app: AppConnection.AWS; app: AppConnection.AWS;
}; };
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema; export type TValidateAwsConnectionCredentialsSchema = typeof ValidateAwsConnectionCredentialsSchema;
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & { export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
orgId: string; orgId: string;
@@ -16,7 +16,7 @@ export type TAzureAppConfigurationConnectionInput = z.infer<typeof CreateAzureAp
app: AppConnection.AzureAppConfiguration; app: AppConnection.AzureAppConfiguration;
}; };
export type TValidateAzureAppConfigurationConnectionCredentials = export type TValidateAzureAppConfigurationConnectionCredentialsSchema =
typeof ValidateAzureAppConfigurationConnectionCredentialsSchema; typeof ValidateAzureAppConfigurationConnectionCredentialsSchema;
export type TAzureAppConfigurationConnectionConfig = DiscriminativePick< export type TAzureAppConfigurationConnectionConfig = DiscriminativePick<
@@ -16,7 +16,7 @@ export type TAzureKeyVaultConnectionInput = z.infer<typeof CreateAzureKeyVaultCo
app: AppConnection.AzureKeyVault; app: AppConnection.AzureKeyVault;
}; };
export type TValidateAzureKeyVaultConnectionCredentials = typeof ValidateAzureKeyVaultConnectionCredentialsSchema; export type TValidateAzureKeyVaultConnectionCredentialsSchema = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
export type TAzureKeyVaultConnectionConfig = DiscriminativePick< export type TAzureKeyVaultConnectionConfig = DiscriminativePick<
TAzureKeyVaultConnectionInput, TAzureKeyVaultConnectionInput,
@@ -15,7 +15,7 @@ export type TDatabricksConnectionInput = z.infer<typeof CreateDatabricksConnecti
app: AppConnection.Databricks; app: AppConnection.Databricks;
}; };
export type TValidateDatabricksConnectionCredentials = typeof ValidateDatabricksConnectionCredentialsSchema; export type TValidateDatabricksConnectionCredentialsSchema = typeof ValidateDatabricksConnectionCredentialsSchema;
export type TDatabricksConnectionConfig = DiscriminativePick< export type TDatabricksConnectionConfig = DiscriminativePick<
TDatabricksConnection, TDatabricksConnection,
@@ -15,7 +15,7 @@ export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
app: AppConnection.GCP; app: AppConnection.GCP;
}; };
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema; export type TValidateGcpConnectionCredentialsSchema = typeof ValidateGcpConnectionCredentialsSchema;
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & { export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
orgId: string; orgId: string;
@@ -15,6 +15,6 @@ export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema
app: AppConnection.GitHub; app: AppConnection.GitHub;
}; };
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema; export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema;
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">; export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
@@ -15,7 +15,7 @@ export type THumanitecConnectionInput = z.infer<typeof CreateHumanitecConnection
app: AppConnection.Humanitec; app: AppConnection.Humanitec;
}; };
export type TValidateHumanitecConnectionCredentials = typeof ValidateHumanitecConnectionCredentialsSchema; export type TValidateHumanitecConnectionCredentialsSchema = typeof ValidateHumanitecConnectionCredentialsSchema;
export type THumanitecConnectionConfig = DiscriminativePick< export type THumanitecConnectionConfig = DiscriminativePick<
THumanitecConnectionInput, THumanitecConnectionInput,
@@ -13,4 +13,4 @@ export type TMsSqlConnectionInput = z.infer<typeof CreateMsSqlConnectionSchema>
app: AppConnection.MsSql; app: AppConnection.MsSql;
}; };
export type TValidateMsSqlConnectionCredentials = typeof ValidateMsSqlConnectionCredentialsSchema; export type TValidateMsSqlConnectionCredentialsSchema = typeof ValidateMsSqlConnectionCredentialsSchema;
@@ -13,4 +13,4 @@ export type TPostgresConnectionInput = z.infer<typeof CreatePostgresConnectionSc
app: AppConnection.Postgres; app: AppConnection.Postgres;
}; };
export type TValidatePostgresConnectionCredentials = typeof ValidatePostgresConnectionCredentialsSchema; export type TValidatePostgresConnectionCredentialsSchema = typeof ValidatePostgresConnectionCredentialsSchema;
@@ -18,10 +18,7 @@ const SQL_CONNECTION_CLIENT_MAP = {
[AppConnection.MsSql]: "mssql" [AppConnection.MsSql]: "mssql"
}; };
export const getSqlConnectionClient = async ( export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection, "credentials" | "app">) => {
appConnection: Pick<TSqlConnection, "credentials" | "app">,
options?: Record<string, unknown>
) => {
const { const {
app, app,
credentials: { host: baseHost, database, port, sslCertificate, password, username } credentials: { host: baseHost, database, port, sslCertificate, password, username }
@@ -41,7 +38,15 @@ export const getSqlConnectionClient = async (
password, password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl, ssl,
options // following dynamic secret mssql driver requirements (see sql-database.ts)
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
options:
app === AppConnection.MsSql
? {
trustServerCertificate: !sslCertificate,
cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {}
}
: undefined
} }
}); });
@@ -548,6 +548,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
try { try {
const secrets = await (tx || db.replicaNode())(TableName.SecretV2) const secrets = await (tx || db.replicaNode())(TableName.SecretV2)
.where({ folderId }) .where({ folderId })
.where((bd) => { .where((bd) => {
query.forEach((el) => { query.forEach((el) => {
if (el.type === SecretType.Personal && !el.userId) { if (el.type === SecretType.Personal && !el.userId) {
@@ -559,10 +560,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
userId: el.type === SecretType.Personal ? el.userId : null userId: el.type === SecretType.Personal ? el.userId : null
}); });
}); });
}); })
return secrets; .leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretV2}.id`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
return secrets.map((secret) => ({
...secret,
isRotatedSecret: Boolean(secret.rotationId)
}));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "find by blind indexes" }); throw new DatabaseError({ error, name: "find by secret keys" });
} }
}; };
@@ -2234,6 +2234,10 @@ export const secretV2BridgeServiceFactory = ({
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
sourceSecrets.forEach((secret) => { sourceSecrets.forEach((secret) => {
if (secret.isRotatedSecret) {
throw new BadRequestError({ message: `Cannot move rotated secret: ${secret.key}` });
}
for (const sourceAction of sourceActions) { for (const sourceAction of sourceActions) {
if ( if (
sourceAction === ProjectPermissionSecretActions.DescribeSecret || sourceAction === ProjectPermissionSecretActions.DescribeSecret ||
@@ -4,7 +4,7 @@ import { format, formatDistanceToNow } from "date-fns";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Tooltip } from "@app/components/v2"; import { Tooltip } from "@app/components/v2";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge"; import { Badge } from "@app/components/v2/Badge/Badge";
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
type Props = { type Props = {
@@ -78,40 +78,30 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
const daysToRotation = const daysToRotation =
(new Date(nextRotationAt).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24); (new Date(nextRotationAt).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24);
let variant: BadgeProps["variant"];
let label: string;
let tooltipContent: string;
if (daysToRotation >= 7) {
variant = "success";
label = `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`;
tooltipContent = `Rotates ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
} else if (daysToRotation < 0) {
variant = "primary";
label = "Rotating";
tooltipContent = `Rotates on ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
} else if (daysToRotation < 1) {
variant = "primary";
label = `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`;
tooltipContent = `Rotates on ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
} else {
variant = "primary";
label = `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`;
tooltipContent = `Rotates on ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
}
return ( return (
<Tooltip className="max-w-lg" content={tooltipContent}> <Tooltip
className="max-w-lg"
content={
<>
<span>
Rotates on {format(nextRotationAt, "MM/dd/yyyy")} at {format(nextRotationAt, "h:mm aa")}
</span>{" "}
<span className="text-mineshaft-300">(Local Time)</span>
</>
}
>
<div> <div>
<Badge <Badge
variant={variant} variant={daysToRotation >= 7 ? "success" : "primary"}
className={twMerge( className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize", "flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className className
)} )}
> >
<FontAwesomeIcon icon={faRotate} /> <FontAwesomeIcon icon={faRotate} />
{label} {daysToRotation < 0
? "Rotating"
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
</Badge> </Badge>
</div> </div>
</Tooltip> </Tooltip>
@@ -59,11 +59,12 @@ const Content = ({ secretRotation }: ContentProps) => {
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
{Component} {Component}
{nextRotationAt && ( {nextRotationAt && (
<div className="flex items-center gap-x-1.5 text-sm text-mineshaft-300"> <div className="flex items-center gap-x-1.5 text-sm text-mineshaft-200">
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" /> <FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
<span> <span>
Next rotation occurs on: {format(nextRotationAt, "MM/dd/yyyy")} at{" "} Next rotation occurs on: {format(nextRotationAt, "MM/dd/yyyy")} at{" "}
{format(nextRotationAt, "h:mm aa")} {format(nextRotationAt, "h:mm aa")}{" "}
<span className="text-mineshaft-300">(Local Time)</span>
</span> </span>
</div> </div>
)} )}
@@ -14,9 +14,7 @@ type Props = {
}; };
export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => { export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => {
const { control, watch } = useFormContext<TSecretRotationV2Form>(); const { control } = useFormContext<TSecretRotationV2Form>();
console.log(watch("rotateAtUtc"));
return ( return (
<> <>
@@ -44,7 +44,24 @@ export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
label={`${connectionName} Connection`} label={`${connectionName} Connection`}
helperText={isUpdate ? "Cannot be updated" : undefined} helperText={
isUpdate ? (
"Cannot be updated"
) : (
<p>
Check out{" "}
<a
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
target="_blank"
className="underline"
rel="noopener noreferrer"
>
our docs
</a>{" "}
to ensure your connection has the required permissions for secret rotation.
</p>
)
}
> >
<FilterableSelect <FilterableSelect
value={value} value={value}
@@ -46,10 +46,17 @@ export const SqlRotationParametersFields = () => {
/> />
<NoticeBannerV2 title="Example Create User Statement"> <NoticeBannerV2 title="Example Create User Statement">
<p className="mb-3 text-sm text-mineshaft-300"> <p className="mb-3 text-sm text-mineshaft-300">
Infisical requires two database users to be created for rotation. Below is an example Infisical requires two database users to be created for rotation.
statement for creating the required users. You may need to modify it to suit your needs.
</p> </p>
<p className="text-sm"> <p className="mb-3 text-sm text-mineshaft-300">
These users are intended to be solely managed by Infisical. Altering their login after
rotation may cause unexpected failure.
</p>
<p className="mb-3 text-sm text-mineshaft-300">
Below is an example statement for creating the required users. You may need to modify it
to suit your needs.
</p>
<p className="mb-3 text-sm">
<pre className="whitespace-pre-wrap rounded border border-mineshaft-700 bg-mineshaft-800 p-2 text-mineshaft-300"> <pre className="whitespace-pre-wrap rounded border border-mineshaft-700 bg-mineshaft-800 p-2 text-mineshaft-300">
{rotationOption!.template.createUserStatement} {rotationOption!.template.createUserStatement}
</pre> </pre>
@@ -1,15 +1,22 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
type Props = { type Props = {
title: string; title: string;
children: ReactNode; children: ReactNode;
className?: string;
}; };
export const NoticeBannerV2 = ({ title, children }: Props) => { export const NoticeBannerV2 = ({ title, children, className }: Props) => {
return ( return (
<div className="flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5"> <div
className={twMerge(
"flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5",
className
)}
>
<div className="mb-1 flex items-center text-sm"> <div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" /> <FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
{title} {title}
+7 -11
View File
@@ -14,15 +14,11 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql [SecretRotation.MsSqlCredentials]: AppConnection.MsSql
}; };
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
new Date( const now = new Date();
Date.UTC(
new Date().getUTCFullYear(), // convert utc rotation time to local datetime
new Date().getUTCMonth(), return new Date(
new Date().getUTCDate(), Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hours, minutes, 0, 0)
hours,
minutes,
0,
0
)
); );
};
@@ -32,6 +32,7 @@ export type TSecretApprovalSecChange = {
secretKey: string; secretKey: string;
secretValue?: string; secretValue?: string;
secretComment?: string; secretComment?: string;
isRotatedSecret?: boolean;
tags?: string[]; tags?: string[];
}; };
@@ -2,6 +2,7 @@
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { SecretType, SecretV3RawSanitized } from "../secrets/types"; import { SecretType, SecretV3RawSanitized } from "../secrets/types";
import { import {
@@ -82,7 +83,8 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
createdAt: secretVersion.createdAt, createdAt: secretVersion.createdAt,
updatedAt: secretVersion.updatedAt, updatedAt: secretVersion.updatedAt,
type: "modified", type: "modified",
version: secretVersion.version version: secretVersion.version,
isRotatedSecret: secretVersion.isRotatedSecret
}; };
if (secretVersion.type === SecretType.Personal) { if (secretVersion.type === SecretType.Personal) {
@@ -162,6 +164,12 @@ export const usePerformSecretRollback = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory }) queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory })
}); });
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({
projectId: workspaceId,
secretPath: directory ?? "/"
})
});
} }
}); });
}; };
@@ -11,7 +11,7 @@ export type TSecretSnapshot = {
export type TSnapshotData = Omit<TSecretSnapshot, "secretVersions"> & { export type TSnapshotData = Omit<TSecretSnapshot, "secretVersions"> & {
id: string; id: string;
secretVersions: SecretVersions[]; secretVersions: (SecretVersions & { isRotatedSecret?: boolean })[];
folderVersion: Array<{ name: string; id: string }>; folderVersion: Array<{ name: string; id: string }>;
environment: WorkspaceEnv; environment: WorkspaceEnv;
}; };
@@ -1,5 +1,6 @@
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2"; import { DeleteActionModal } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { TAppConnection, useDeleteAppConnection } from "@app/hooks/api/appConnections"; import { TAppConnection, useDeleteAppConnection } from "@app/hooks/api/appConnections";
@@ -46,6 +47,17 @@ export const DeleteAppConnectionModal = ({ isOpen, onOpenChange, appConnection }
title={`Are you sure want to delete ${name}?`} title={`Are you sure want to delete ${name}?`}
deleteKey={name} deleteKey={name}
onDeleteApproved={handleDeleteAppConnection} onDeleteApproved={handleDeleteAppConnection}
/> >
{appConnection.isPlatformManagedCredentials && (
<NoticeBannerV2 className="mt-3" title="Platform Managed Credentials">
<p className="text-sm text-bunker-300">
This App Connection&#39;s credentials are managed by Infisical.
</p>
<p className="mt-3 text-sm text-bunker-300">
By deleting this connection you may lose permanent access to the associated resource.
</p>
</NoticeBannerV2>
)}
</DeleteActionModal>
); );
}; };
@@ -97,7 +97,13 @@ export const SecretApprovalRequestChangeItem = ({
<Td className="text-red-600">OLD</Td> <Td className="text-red-600">OLD</Td>
<Td>{secretVersion?.secretKey}</Td> <Td>{secretVersion?.secretKey}</Td>
<Td> <Td>
<SecretInput isReadOnly value={secretVersion?.secretValue} /> {newVersion?.isRotatedSecret ? (
<span className="text-mineshaft-400">
Rotated Secret value will not be affected
</span>
) : (
<SecretInput isReadOnly value={secretVersion?.secretValue} />
)}
</Td> </Td>
<Td>{secretVersion?.secretComment}</Td> <Td>{secretVersion?.secretComment}</Td>
<Td className="flex flex-wrap gap-2"> <Td className="flex flex-wrap gap-2">
@@ -146,7 +152,13 @@ export const SecretApprovalRequestChangeItem = ({
<Td className="text-green-600">NEW</Td> <Td className="text-green-600">NEW</Td>
<Td>{newVersion?.secretKey}</Td> <Td>{newVersion?.secretKey}</Td>
<Td> <Td>
<SecretInput isReadOnly value={newVersion?.secretValue} /> {newVersion?.isRotatedSecret ? (
<span className="text-mineshaft-400">
Rotated Secret value will not be affected
</span>
) : (
<SecretInput isReadOnly value={newVersion?.secretValue} />
)}
</Td> </Td>
<Td>{newVersion?.secretComment}</Td> <Td>{newVersion?.secretComment}</Td>
<Td className="flex flex-wrap gap-2"> <Td className="flex flex-wrap gap-2">
@@ -76,7 +76,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<FontAwesomeIcon icon={faKey} /> <FontAwesomeIcon icon={faKey} />
</div> </div>
<div className="flex flex-grow items-center space-x-4 px-4 py-3"> <div className="flex flex-grow items-center space-x-4 px-4 py-3">
{mode === "modified" ? ( {mode === "modified" && !preSecret?.isRotatedSecret ? (
<> <>
<div>{preSecret?.key}</div> <div>{preSecret?.key}</div>
<div className="rounded-lg bg-primary px-1 py-0.5 text-xs font-bold text-black"> <div className="rounded-lg bg-primary px-1 py-0.5 text-xs font-bold text-black">
@@ -90,7 +90,14 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
</div> </div>
</> </>
) : ( ) : (
postSecret.key <>
{postSecret.key}
{postSecret.isRotatedSecret && (
<span className="ml-2 text-mineshaft-400">
Rotated Secrets are not affected by Rollback
</span>
)}
</>
)} )}
</div> </div>
</div> </div>
@@ -33,11 +33,13 @@ type Props = {
const LOADER_TEXT = ["Fetching your snapshot", "Creating the difference view"]; const LOADER_TEXT = ["Fetching your snapshot", "Creating the difference view"];
const deepCompareSecrets = (lhs: SecretV3RawSanitized, rhs: SecretV3RawSanitized) => const deepCompareSecrets = (lhs: SecretV3RawSanitized, rhs: SecretV3RawSanitized) =>
lhs.key === rhs.key && lhs.isRotatedSecret ||
lhs.value === rhs.value && rhs.isRotatedSecret ||
lhs.comment === rhs.comment && (lhs.key === rhs.key &&
lhs?.valueOverride === rhs?.valueOverride && lhs.value === rhs.value &&
JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags); lhs.comment === rhs.comment &&
lhs?.valueOverride === rhs?.valueOverride &&
JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags));
export const SnapshotView = ({ export const SnapshotView = ({
snapshotId, snapshotId,