mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 03:26:27 +00:00
improvements: address feedback
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+11
-3
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+1
-1
@@ -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<
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+3
-2
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+1
-3
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
+18
-1
@@ -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}
|
||||||
|
|||||||
+10
-3
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-1
@@ -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'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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-2
@@ -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">
|
||||||
|
|||||||
+9
-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>
|
||||||
|
|||||||
+7
-5
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user