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

View File

@@ -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}`);
}
}

View File

@@ -277,8 +277,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
secretPath: z.string(),
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({
secretValue: z.string().optional(),
isRotatedSecret: z.boolean().optional(),
op: z.string(),
tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish(),

View File

@@ -33,7 +33,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
.extend({
secretValueHidden: z.boolean(),
secretId: z.string(),
tags: SanitizedTagSchema.array()
tags: SanitizedTagSchema.array(),
isRotatedSecret: z.boolean().optional()
})
.array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),

View File

@@ -151,7 +151,7 @@ export const registerSecretRotationEndpoints = <
rateLimit: readLimit
},
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({
rotationName: z
.string()

View File

@@ -13,7 +13,8 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat(
(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

View File

@@ -257,6 +257,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema("secVerTag")
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretV2}.id`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({
secVerTagId: "secVerTag.id",
@@ -285,7 +290,8 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
)
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
const formatedDoc = sqlNestRelationships({
data: doc,
key: "id",
@@ -304,14 +310,16 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{
key: "secretId",
label: "secret" as const,
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) =>
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId, rotationId }) =>
secretId
? {
id: secretId,
version: orgSecVersion,
key: orgSecKey,
encryptedValue: orgSecValue,
encryptedComment: orgSecComment
encryptedComment: orgSecComment,
isRotatedSecret: Boolean(rotationId),
rotationId
}
: undefined
},

View File

@@ -262,7 +262,13 @@ export const secretApprovalRequestServiceFactory = ({
id: el.id,
version: el.version,
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
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: "",
@@ -609,7 +615,7 @@ export const secretApprovalRequestServiceFactory = ({
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
typeof el.encryptedValue !== "undefined"
!el.secret.isRotatedSecret && typeof el.encryptedValue !== "undefined"
? {
encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue

View File

@@ -189,9 +189,11 @@ export const secretRotationV2DALFactory = (
.countDistinct(`${TableName.SecretRotationV2}.name`);
if (search) {
void query
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
void query.where((qb) => {
void qb
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
});
}
const result = await query;

View File

@@ -23,7 +23,7 @@ export const listSecretRotationOptions = () => {
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();
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();
return new Date(
Date.UTC(
@@ -58,10 +58,10 @@ export const getNextUtcRotationInterval = (rotateAtUtc?: TSecretRotationV2["rota
const appCfg = getConfig();
if (appCfg.isRotationDevelopmentMode) {
return getNextUTCMinute(rotateAtUtc);
return getNextUTCMinuteInterval(rotateAtUtc);
}
return getNextUTCMidnight(rotateAtUtc);
return getNextUTCDayInterval(rotateAtUtc);
};
export const encryptSecretRotationCredentials = async ({

View File

@@ -80,7 +80,7 @@ export const secretRotationV2QueueServiceFactory = async ({
{
batchSize: 1,
workerCount: 1,
pollingIntervalSeconds: 0.5
pollingIntervalSeconds: appCfg.isRotationDevelopmentMode ? 0.5 : 30
}
);
@@ -122,7 +122,7 @@ export const secretRotationV2QueueServiceFactory = async ({
},
{
batchSize: 1,
workerCount: 30,
workerCount: 2,
pollingIntervalSeconds: 0.5
}
);
@@ -179,8 +179,8 @@ export const secretRotationV2QueueServiceFactory = async ({
},
{
batchSize: 1,
workerCount: 5,
pollingIntervalSeconds: 30
workerCount: 2,
pollingIntervalSeconds: 1
}
);

View File

@@ -1,4 +1,5 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Knex } from "knex";
import isEqual from "lodash.isequal";
import { ActionProjectType, SecretType, TableName } from "@app/db/schemas";
@@ -46,7 +47,7 @@ import {
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { sqlCredentialsRotationFactory } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
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 { DatabaseErrorCode } from "@app/lib/error-codes";
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 (
{ projectId, type }: TListSecretRotationsV2ByProjectId,
actor: OrgServiceActor
@@ -345,6 +377,7 @@ export const secretRotationV2ServiceFactory = ({
secretPath,
environment,
rotateAtUtc = { hours: 0, minutes: 0 },
secretsMapping,
...payload
}: TCreateSecretRotationV2DTO,
actor: OrgServiceActor
@@ -368,7 +401,10 @@ export const secretRotationV2ServiceFactory = ({
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
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(
ProjectPermissionSecretRotationActions.Create,
@@ -389,7 +425,7 @@ export const secretRotationV2ServiceFactory = ({
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({
parameters: payload.parameters,
secretsMapping: payload.secretsMapping,
secretsMapping,
connection
} as TSecretRotationV2WithConnection);
@@ -405,9 +441,19 @@ export const secretRotationV2ServiceFactory = ({
});
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(
{
folderId: folder.id,
secretsMapping,
...payload,
encryptedGeneratedCredentials,
rotateAtUtc,
@@ -483,12 +529,6 @@ export const secretRotationV2ServiceFactory = ({
throw new BadRequestError({
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:
throw err;
}
@@ -497,6 +537,8 @@ export const secretRotationV2ServiceFactory = ({
throw err;
}
if (err instanceof BadRequestError) throw err;
throw new BadRequestError({
message: parseRotationErrorMessage(err)
});
@@ -521,7 +563,8 @@ export const secretRotationV2ServiceFactory = ({
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({
actor: actor.type,
@@ -551,26 +594,36 @@ export const secretRotationV2ServiceFactory = ({
isManualRotation: false
});
let secretsMappingUpdated = false;
try {
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)) {
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
await fnSecretBulkUpdate({
folderId,
orgId: connection.orgId,
tx,
inputSecrets: Object.entries(secretsMapping as TSecretRotationV2["secretsMapping"]).map(
([mappingKey, secretKey]) => ({
filter: {
key: secretKey,
folderId,
type: SecretType.Shared
},
data: {
key: payload.secretsMapping![mappingKey as keyof TSecretRotationV2["secretsMapping"]]
}
})
),
inputSecrets: Object.entries(secretsMapping).map(([mappingKey, secretKey]) => ({
filter: {
key: secretKey,
folderId,
type: SecretType.Shared
},
data: {
key: payload.secretsMapping![mappingKey as keyof TSecretRotationV2["secretsMapping"]]
}
})),
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
@@ -578,14 +631,7 @@ export const secretRotationV2ServiceFactory = ({
resourceMetadataDAL
});
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({
orgId: connection.orgId,
secretPath: folder.path,
projectId,
environmentSlug: environment.slug,
excludeReplication: true
});
secretsMappingUpdated = true;
}
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
if (nextRotationAt && nextRotationAt.getTime() < getNextUtcRotationInterval().getTime()) {
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}"`
});
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:
throw err;
}
}
}
if (err instanceof BadRequestError) throw err;
throw err;
}
};
@@ -695,15 +746,6 @@ export const secretRotationV2ServiceFactory = ({
actorId: actor.id, // not actually used since rotated secrets are shared
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);
@@ -728,6 +770,17 @@ export const secretRotationV2ServiceFactory = ({
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);
};

View File

@@ -398,8 +398,32 @@ export const secretSnapshotServiceFactory = ({
if (shouldUseBridge) {
const rollback = await snapshotDAL.transaction(async (tx) => {
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
// this will remove all secrets in current folder
const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx);
const secretRotationIds = rollbackSnaps
.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);
// 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
@@ -424,28 +448,31 @@ export const secretSnapshotServiceFactory = ({
);
const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map(
({
latestSecretVersion,
version,
updatedAt,
createdAt,
secretId,
envId,
id,
tags,
// exclude the bottom fields from the secret - they are for versioning only.
userActorId,
identityActorId,
actorType,
...el
}) => ({
...el,
id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
folderId
})
)
secretVersions
.filter((v) => !v.isRotatedSecret)
.map(
({
latestSecretVersion,
version,
updatedAt,
createdAt,
secretId,
envId,
id,
tags,
// exclude the bottom fields from the secret - they are for versioning only.
userActorId,
identityActorId,
actorType,
isRotatedSecret,
...el
}) => ({
...el,
id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
folderId
})
)
),
tx
);

View File

@@ -181,6 +181,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretRotationV2SecretMapping}.secretId`,
`${TableName.SecretVersionV2}.secretId`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(
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.SecretVersionV2Tag).as("tagVersionId"),
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({
data,
@@ -221,7 +227,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => SecretVersionsV2Schema.parse(el),
mapper: (el) => ({
...SecretVersionsV2Schema.parse(el),
isRotatedSecret: Boolean(el.rotationId),
rotationId: el.rotationId
}),
childrenMapper: [
{
key: "tagVersionId",
@@ -476,6 +486,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretVersionV2}.secretId`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.leftJoin<{ latestSecretVersion: number }>(
(tx || db)(TableName.SecretVersionV2)
.groupBy("secretId")
@@ -506,7 +521,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
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({
@@ -523,7 +539,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
label: "secretVersions" as const,
mapper: (el) => ({
...SecretVersionsV2Schema.parse(el),
latestSecretVersion: el.latestSecretVersion as number
latestSecretVersion: el.latestSecretVersion as number,
isRotatedSecret: Boolean(el.rotationId)
}),
childrenMapper: [
{

View File

@@ -8,7 +8,8 @@ export const PgSqlLock = {
SuperAdminInit: 2024,
KmsRootKeyInit: 2025,
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;
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;

View File

@@ -1,6 +1,8 @@
import { URL } from "url"; // Import the URL class
export const getDbConnectionHost = (urlString: string) => {
export const getDbConnectionHost = (urlString?: string) => {
if (!urlString) return null;
try {
const url = new URL(urlString);
// Split hostname and port (if provided)

View File

@@ -2,11 +2,17 @@ import { Knex } from "knex";
import { UnauthorizedError } from "../errors";
type TKnexDynamicPrimitiveOperator<T extends object> = {
operator: "eq" | "ne" | "startsWith" | "endsWith";
value: string;
field: Extract<keyof T, string>;
};
type TKnexDynamicPrimitiveOperator<T extends object> =
| {
operator: "eq" | "ne" | "startsWith" | "endsWith";
value: string;
field: Extract<keyof T, string>;
}
| {
operator: "notIn";
value: string[];
field: Extract<keyof T, string>;
};
type TKnexDynamicInOperator<T extends object> = {
operator: "in";
@@ -48,6 +54,10 @@ export const buildDynamicKnexQuery = <T extends object>(
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
break;
}
case "notIn": {
void queryBuilder.whereNotIn(filterAst.field, filterAst.value);
break;
}
case "and": {
filterAst.value.forEach((el) => {
void queryBuilder.andWhere((subQueryBuilder) => {

View File

@@ -25,7 +25,7 @@ import {
TAppConnectionRaw,
TCreateAppConnectionDTO,
TUpdateAppConnectionDTO,
TValidateAppConnectionCredentials
TValidateAppConnectionCredentialsSchema
} from "./app-connection-types";
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service";
@@ -50,7 +50,7 @@ export type TAppConnectionServiceFactoryDep = {
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.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
@@ -170,26 +170,22 @@ export const appConnectionServiceFactory = ({
} as TAppConnectionConfig);
try {
const createTransaction = (connectionCredentials: TAppConnection["credentials"]) =>
appConnectionDAL.transaction(async (tx) => {
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: connectionCredentials,
orgId: actor.orgId,
kmsService
});
return appConnectionDAL.create(
{
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
},
tx
);
const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: connectionCredentials,
orgId: actor.orgId,
kmsService
});
return appConnectionDAL.create({
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
});
};
let connection: TAppConnectionRaw;
if (params.isPlatformManagedCredentials) {
@@ -200,10 +196,10 @@ export const appConnectionServiceFactory = ({
credentials: validatedCredentials,
method
} as TAppConnectionConfig,
(platformCredentials) => createTransaction(platformCredentials)
(platformCredentials) => createConnection(platformCredentials)
);
} else {
connection = await createTransaction(validatedCredentials);
connection = await createConnection(validatedCredentials);
}
return {
@@ -277,26 +273,21 @@ export const appConnectionServiceFactory = ({
}
try {
const updateTransaction = (connectionCredentials: TAppConnection["credentials"] | undefined) =>
appConnectionDAL.transaction(async (tx) => {
const encryptedCredentials = connectionCredentials
? await encryptAppConnectionCredentials({
credentials: connectionCredentials,
orgId: actor.orgId,
kmsService
})
: undefined;
return appConnectionDAL.updateById(
connectionId,
{
const updateConnection = async (connectionCredentials: TAppConnection["credentials"] | undefined) => {
const encryptedCredentials = connectionCredentials
? await encryptAppConnectionCredentials({
credentials: connectionCredentials,
orgId: actor.orgId,
encryptedCredentials,
...params
},
tx
);
kmsService
})
: undefined;
return appConnectionDAL.updateById(connectionId, {
orgId: actor.orgId,
encryptedCredentials,
...params
});
};
let updatedConnection: TAppConnectionRaw;
@@ -312,10 +303,10 @@ export const appConnectionServiceFactory = ({
credentials: updatedCredentials,
method
} as TAppConnectionConfig,
(platformCredentials) => updateTransaction(platformCredentials)
(platformCredentials) => updateConnection(platformCredentials)
);
} else {
updatedConnection = await updateTransaction(updatedCredentials);
updatedConnection = await updateConnection(updatedCredentials);
}
return await decryptAppConnection(updatedConnection, kmsService);

View File

@@ -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 { AWSRegion } from "./app-connection-enums";
import { TAwsConnection, TAwsConnectionConfig, TAwsConnectionInput, TValidateAwsConnectionCredentials } from "./aws";
import {
TAwsConnection,
TAwsConnectionConfig,
TAwsConnectionInput,
TValidateAwsConnectionCredentialsSchema
} from "./aws";
import {
TAzureAppConfigurationConnection,
TAzureAppConfigurationConnectionConfig,
TAzureAppConfigurationConnectionInput,
TValidateAzureAppConfigurationConnectionCredentials
TValidateAzureAppConfigurationConnectionCredentialsSchema
} from "./azure-app-configuration";
import {
TAzureKeyVaultConnection,
TAzureKeyVaultConnectionConfig,
TAzureKeyVaultConnectionInput,
TValidateAzureKeyVaultConnectionCredentials
TValidateAzureKeyVaultConnectionCredentialsSchema
} from "./azure-key-vault";
import {
TDatabricksConnection,
TDatabricksConnectionConfig,
TDatabricksConnectionInput,
TValidateDatabricksConnectionCredentials
TValidateDatabricksConnectionCredentialsSchema
} from "./databricks";
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
import {
TGcpConnection,
TGcpConnectionConfig,
TGcpConnectionInput,
TValidateGcpConnectionCredentialsSchema
} from "./gcp";
import {
TGitHubConnection,
TGitHubConnectionConfig,
TGitHubConnectionInput,
TValidateGitHubConnectionCredentials
TValidateGitHubConnectionCredentialsSchema
} from "./github";
import {
THumanitecConnection,
THumanitecConnectionConfig,
THumanitecConnectionInput,
TValidateHumanitecConnectionCredentials
TValidateHumanitecConnectionCredentialsSchema
} from "./humanitec";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentials } from "./mssql";
import { TPostgresConnection, TPostgresConnectionInput, TValidatePostgresConnectionCredentials } from "./postgres";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TPostgresConnection,
TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema
} from "./postgres";
export type TAppConnection = { id: string } & (
| TAwsConnection
@@ -87,16 +101,16 @@ export type TAppConnectionConfig =
| THumanitecConnectionConfig
| TSqlConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
| TValidateGitHubConnectionCredentials
| TValidateGcpConnectionCredentials
| TValidateAzureKeyVaultConnectionCredentials
| TValidateAzureAppConfigurationConnectionCredentials
| TValidateDatabricksConnectionCredentials
| TValidateHumanitecConnectionCredentials
| TValidatePostgresConnectionCredentials
| TValidateMsSqlConnectionCredentials;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
| TValidateGitHubConnectionCredentialsSchema
| TValidateGcpConnectionCredentialsSchema
| TValidateAzureKeyVaultConnectionCredentialsSchema
| TValidateAzureAppConfigurationConnectionCredentialsSchema
| TValidateDatabricksConnectionCredentialsSchema
| TValidateHumanitecConnectionCredentialsSchema
| TValidatePostgresConnectionCredentialsSchema
| TValidateMsSqlConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;

View File

@@ -15,7 +15,7 @@ export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
app: AppConnection.AWS;
};
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
export type TValidateAwsConnectionCredentialsSchema = typeof ValidateAwsConnectionCredentialsSchema;
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;

View File

@@ -16,7 +16,7 @@ export type TAzureAppConfigurationConnectionInput = z.infer<typeof CreateAzureAp
app: AppConnection.AzureAppConfiguration;
};
export type TValidateAzureAppConfigurationConnectionCredentials =
export type TValidateAzureAppConfigurationConnectionCredentialsSchema =
typeof ValidateAzureAppConfigurationConnectionCredentialsSchema;
export type TAzureAppConfigurationConnectionConfig = DiscriminativePick<

View File

@@ -16,7 +16,7 @@ export type TAzureKeyVaultConnectionInput = z.infer<typeof CreateAzureKeyVaultCo
app: AppConnection.AzureKeyVault;
};
export type TValidateAzureKeyVaultConnectionCredentials = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
export type TValidateAzureKeyVaultConnectionCredentialsSchema = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
export type TAzureKeyVaultConnectionConfig = DiscriminativePick<
TAzureKeyVaultConnectionInput,

View File

@@ -15,7 +15,7 @@ export type TDatabricksConnectionInput = z.infer<typeof CreateDatabricksConnecti
app: AppConnection.Databricks;
};
export type TValidateDatabricksConnectionCredentials = typeof ValidateDatabricksConnectionCredentialsSchema;
export type TValidateDatabricksConnectionCredentialsSchema = typeof ValidateDatabricksConnectionCredentialsSchema;
export type TDatabricksConnectionConfig = DiscriminativePick<
TDatabricksConnection,

View File

@@ -15,7 +15,7 @@ export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
app: AppConnection.GCP;
};
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema;
export type TValidateGcpConnectionCredentialsSchema = typeof ValidateGcpConnectionCredentialsSchema;
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;

View File

@@ -15,6 +15,6 @@ export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema
app: AppConnection.GitHub;
};
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema;
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;

View File

@@ -15,7 +15,7 @@ export type THumanitecConnectionInput = z.infer<typeof CreateHumanitecConnection
app: AppConnection.Humanitec;
};
export type TValidateHumanitecConnectionCredentials = typeof ValidateHumanitecConnectionCredentialsSchema;
export type TValidateHumanitecConnectionCredentialsSchema = typeof ValidateHumanitecConnectionCredentialsSchema;
export type THumanitecConnectionConfig = DiscriminativePick<
THumanitecConnectionInput,

View File

@@ -13,4 +13,4 @@ export type TMsSqlConnectionInput = z.infer<typeof CreateMsSqlConnectionSchema>
app: AppConnection.MsSql;
};
export type TValidateMsSqlConnectionCredentials = typeof ValidateMsSqlConnectionCredentialsSchema;
export type TValidateMsSqlConnectionCredentialsSchema = typeof ValidateMsSqlConnectionCredentialsSchema;

View File

@@ -13,4 +13,4 @@ export type TPostgresConnectionInput = z.infer<typeof CreatePostgresConnectionSc
app: AppConnection.Postgres;
};
export type TValidatePostgresConnectionCredentials = typeof ValidatePostgresConnectionCredentialsSchema;
export type TValidatePostgresConnectionCredentialsSchema = typeof ValidatePostgresConnectionCredentialsSchema;

View File

@@ -18,10 +18,7 @@ const SQL_CONNECTION_CLIENT_MAP = {
[AppConnection.MsSql]: "mssql"
};
export const getSqlConnectionClient = async (
appConnection: Pick<TSqlConnection, "credentials" | "app">,
options?: Record<string, unknown>
) => {
export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection, "credentials" | "app">) => {
const {
app,
credentials: { host: baseHost, database, port, sslCertificate, password, username }
@@ -41,7 +38,15 @@ export const getSqlConnectionClient = async (
password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
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
}
});

View File

@@ -548,6 +548,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
try {
const secrets = await (tx || db.replicaNode())(TableName.SecretV2)
.where({ folderId })
.where((bd) => {
query.forEach((el) => {
if (el.type === SecretType.Personal && !el.userId) {
@@ -559,10 +560,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
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) {
throw new DatabaseError({ error, name: "find by blind indexes" });
throw new DatabaseError({ error, name: "find by secret keys" });
}
};

View File

@@ -2234,6 +2234,10 @@ export const secretV2BridgeServiceFactory = ({
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
sourceSecrets.forEach((secret) => {
if (secret.isRotatedSecret) {
throw new BadRequestError({ message: `Cannot move rotated secret: ${secret.key}` });
}
for (const sourceAction of sourceActions) {
if (
sourceAction === ProjectPermissionSecretActions.DescribeSecret ||

View File

@@ -4,7 +4,7 @@ import { format, formatDistanceToNow } from "date-fns";
import { twMerge } from "tailwind-merge";
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";
type Props = {
@@ -78,40 +78,30 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
const daysToRotation =
(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 (
<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>
<Badge
variant={variant}
variant={daysToRotation >= 7 ? "success" : "primary"}
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faRotate} />
{label}
{daysToRotation < 0
? "Rotating"
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
</Badge>
</div>
</Tooltip>

View File

@@ -59,11 +59,12 @@ const Content = ({ secretRotation }: ContentProps) => {
<div className="flex flex-col gap-y-4">
{Component}
{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" />
<span>
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>
</div>
)}

View File

@@ -14,9 +14,7 @@ type Props = {
};
export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => {
const { control, watch } = useFormContext<TSecretRotationV2Form>();
console.log(watch("rotateAtUtc"));
const { control } = useFormContext<TSecretRotationV2Form>();
return (
<>

View File

@@ -44,7 +44,24 @@ export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }
isError={Boolean(error)}
errorText={error?.message}
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
value={value}

View File

@@ -46,10 +46,17 @@ export const SqlRotationParametersFields = () => {
/>
<NoticeBannerV2 title="Example Create User Statement">
<p className="mb-3 text-sm text-mineshaft-300">
Infisical requires two database users to be created for rotation. Below is an example
statement for creating the required users. You may need to modify it to suit your needs.
Infisical requires two database users to be created for rotation.
</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">
{rotationOption!.template.createUserStatement}
</pre>

View File

@@ -1,15 +1,22 @@
import { ReactNode } from "react";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
type Props = {
title: string;
children: ReactNode;
className?: string;
};
export const NoticeBannerV2 = ({ title, children }: Props) => {
export const NoticeBannerV2 = ({ title, children, className }: Props) => {
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">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
{title}

View File

@@ -14,15 +14,11 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
};
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) =>
new Date(
Date.UTC(
new Date().getUTCFullYear(),
new Date().getUTCMonth(),
new Date().getUTCDate(),
hours,
minutes,
0,
0
)
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
const now = new Date();
// convert utc rotation time to local datetime
return new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hours, minutes, 0, 0)
);
};

View File

@@ -32,6 +32,7 @@ export type TSecretApprovalSecChange = {
secretKey: string;
secretValue?: string;
secretComment?: string;
isRotatedSecret?: boolean;
tags?: string[];
};

View File

@@ -2,6 +2,7 @@
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { SecretType, SecretV3RawSanitized } from "../secrets/types";
import {
@@ -82,7 +83,8 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
createdAt: secretVersion.createdAt,
updatedAt: secretVersion.updatedAt,
type: "modified",
version: secretVersion.version
version: secretVersion.version,
isRotatedSecret: secretVersion.isRotatedSecret
};
if (secretVersion.type === SecretType.Personal) {
@@ -162,6 +164,12 @@ export const usePerformSecretRollback = () => {
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory })
});
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({
projectId: workspaceId,
secretPath: directory ?? "/"
})
});
}
});
};

View File

@@ -11,7 +11,7 @@ export type TSecretSnapshot = {
export type TSnapshotData = Omit<TSecretSnapshot, "secretVersions"> & {
id: string;
secretVersions: SecretVersions[];
secretVersions: (SecretVersions & { isRotatedSecret?: boolean })[];
folderVersion: Array<{ name: string; id: string }>;
environment: WorkspaceEnv;
};

View File

@@ -1,5 +1,6 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { APP_CONNECTION_MAP } from "@app/helpers/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}?`}
deleteKey={name}
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>
);
};

View File

@@ -97,7 +97,13 @@ export const SecretApprovalRequestChangeItem = ({
<Td className="text-red-600">OLD</Td>
<Td>{secretVersion?.secretKey}</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>{secretVersion?.secretComment}</Td>
<Td className="flex flex-wrap gap-2">
@@ -146,7 +152,13 @@ export const SecretApprovalRequestChangeItem = ({
<Td className="text-green-600">NEW</Td>
<Td>{newVersion?.secretKey}</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>{newVersion?.secretComment}</Td>
<Td className="flex flex-wrap gap-2">

View File

@@ -76,7 +76,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<FontAwesomeIcon icon={faKey} />
</div>
<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 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>
</>
) : (
postSecret.key
<>
{postSecret.key}
{postSecret.isRotatedSecret && (
<span className="ml-2 text-mineshaft-400">
Rotated Secrets are not affected by Rollback
</span>
)}
</>
)}
</div>
</div>

View File

@@ -33,11 +33,13 @@ type Props = {
const LOADER_TEXT = ["Fetching your snapshot", "Creating the difference view"];
const deepCompareSecrets = (lhs: SecretV3RawSanitized, rhs: SecretV3RawSanitized) =>
lhs.key === rhs.key &&
lhs.value === rhs.value &&
lhs.comment === rhs.comment &&
lhs?.valueOverride === rhs?.valueOverride &&
JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags);
lhs.isRotatedSecret ||
rhs.isRotatedSecret ||
(lhs.key === rhs.key &&
lhs.value === rhs.value &&
lhs.comment === rhs.comment &&
lhs?.valueOverride === rhs?.valueOverride &&
JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags));
export const SnapshotView = ({
snapshotId,