mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 19:16:22 +00:00
feature secret rotations v2
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export * from "./mssql-credentials-rotation-constants";
|
||||
export * from "./mssql-credentials-rotation-schemas";
|
||||
export * from "./mssql-credentials-rotation-types";
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const MSSQL_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "Microsoft SQL Server Credentials",
|
||||
type: SecretRotation.MsSqlCredentials,
|
||||
connection: AppConnection.MsSql,
|
||||
template: {
|
||||
createUserStatement: `CREATE LOGIN [my_mssql_user] WITH PASSWORD = 'my_temporary_password'; CREATE USER [my_mssql_user] FOR LOGIN [my_mssql_user]; GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [my_mssql_user];`,
|
||||
secretsMapping: {
|
||||
username: "MSSQL_DB_USERNAME",
|
||||
password: "MSSQL_DB_PASSWORD"
|
||||
}
|
||||
}
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import {
|
||||
SqlCredentialsRotationParametersSchema,
|
||||
SqlCredentialsRotationSecretsMappingSchema,
|
||||
SqlCredentialsRotationTemplateSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const MsSqlCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.MsSqlCredentials).extend({
|
||||
type: z.literal(SecretRotation.MsSqlCredentials),
|
||||
parameters: SqlCredentialsRotationParametersSchema,
|
||||
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateMsSqlCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.MsSqlCredentials
|
||||
).extend({
|
||||
parameters: SqlCredentialsRotationParametersSchema,
|
||||
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateMsSqlCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.MsSqlCredentials
|
||||
).extend({
|
||||
parameters: SqlCredentialsRotationParametersSchema.optional(),
|
||||
secretsMapping: SqlCredentialsRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const MsSqlCredentialsRotationListItemSchema = z.object({
|
||||
name: z.literal("Microsoft SQL Server Credentials"),
|
||||
connection: z.literal(AppConnection.MsSql),
|
||||
type: z.literal(SecretRotation.MsSqlCredentials),
|
||||
template: SqlCredentialsRotationTemplateSchema
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TMsSqlConnection } from "@app/services/app-connection/mssql";
|
||||
|
||||
import {
|
||||
CreateMsSqlCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationListItemSchema,
|
||||
MsSqlCredentialsRotationSchema
|
||||
} from "./mssql-credentials-rotation-schemas";
|
||||
|
||||
export type TMsSqlCredentialsRotation = z.infer<typeof MsSqlCredentialsRotationSchema>;
|
||||
|
||||
export type TMsSqlCredentialsRotationInput = z.infer<typeof CreateMsSqlCredentialsRotationSchema>;
|
||||
|
||||
export type TMsSqlCredentialsRotationListItem = z.infer<typeof MsSqlCredentialsRotationListItemSchema>;
|
||||
|
||||
export type TMsSqlCredentialsRotationWithConnection = TMsSqlCredentialsRotation & {
|
||||
connection: TMsSqlConnection;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./postgres-credentials-rotation-constants";
|
||||
export * from "./postgres-credentials-rotation-schemas";
|
||||
export * from "./postgres-credentials-rotation-types";
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "PostgreSQL Credentials",
|
||||
type: SecretRotation.PostgresCredentials,
|
||||
connection: AppConnection.Postgres,
|
||||
template: {
|
||||
createUserStatement: `CREATE USER "my_pg_user" WITH ENCRYPTED PASSWORD 'temporary_password'; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "my_pg_user";`,
|
||||
secretsMapping: {
|
||||
username: "POSTGRES_DB_USERNAME",
|
||||
password: "POSTGRES_DB_PASSWORD"
|
||||
}
|
||||
}
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import {
|
||||
SqlCredentialsRotationParametersSchema,
|
||||
SqlCredentialsRotationSecretsMappingSchema,
|
||||
SqlCredentialsRotationTemplateSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const PostgresCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.PostgresCredentials).extend({
|
||||
type: z.literal(SecretRotation.PostgresCredentials),
|
||||
parameters: SqlCredentialsRotationParametersSchema,
|
||||
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreatePostgresCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.PostgresCredentials
|
||||
).extend({
|
||||
parameters: SqlCredentialsRotationParametersSchema,
|
||||
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdatePostgresCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.PostgresCredentials
|
||||
).extend({
|
||||
parameters: SqlCredentialsRotationParametersSchema.optional(),
|
||||
secretsMapping: SqlCredentialsRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const PostgresCredentialsRotationListItemSchema = z.object({
|
||||
name: z.literal("PostgreSQL Credentials"),
|
||||
connection: z.literal(AppConnection.Postgres),
|
||||
type: z.literal(SecretRotation.PostgresCredentials),
|
||||
template: SqlCredentialsRotationTemplateSchema
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TPostgresConnection } from "@app/services/app-connection/postgres";
|
||||
|
||||
import {
|
||||
CreatePostgresCredentialsRotationSchema,
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
PostgresCredentialsRotationSchema
|
||||
} from "./postgres-credentials-rotation-schemas";
|
||||
|
||||
export type TPostgresCredentialsRotation = z.infer<typeof PostgresCredentialsRotationSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationInput = z.infer<typeof CreatePostgresCredentialsRotationSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationListItem = z.infer<typeof PostgresCredentialsRotationListItemSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationWithConnection = TPostgresCredentialsRotation & {
|
||||
connection: TPostgresConnection;
|
||||
};
|
||||
@@ -0,0 +1,465 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TSecretRotationsV2 } from "@app/db/schemas/secret-rotations-v2";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
buildFindFilter,
|
||||
ormify,
|
||||
prependTableNameToFindFilter,
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
TFindOpt
|
||||
} from "@app/lib/knex";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
export type TSecretRotationV2DALFactory = ReturnType<typeof secretRotationV2DALFactory>;
|
||||
|
||||
type TSecretRotationFindFilter = Parameters<typeof buildFindFilter<TSecretRotationsV2>>[0];
|
||||
type TSecretRotationFindOptions = TFindOpt<TSecretRotationsV2, true, "name">;
|
||||
|
||||
const baseSecretRotationV2Query = ({
|
||||
filter = {},
|
||||
options,
|
||||
db,
|
||||
tx
|
||||
}: {
|
||||
db: TDbClient;
|
||||
filter?: { projectId?: string } & TSecretRotationFindFilter;
|
||||
options?: TSecretRotationFindOptions;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
const { projectId, ...filters } = filter;
|
||||
|
||||
const query = (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(TableName.AppConnection, `${TableName.SecretRotationV2}.connectionId`, `${TableName.AppConnection}.id`)
|
||||
.select(selectAllTableCols(TableName.SecretRotationV2))
|
||||
.select(
|
||||
// environment
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
// entire connection
|
||||
db.ref("name").withSchema(TableName.AppConnection).as("connectionName"),
|
||||
db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"),
|
||||
db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"),
|
||||
db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"),
|
||||
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db
|
||||
.ref("isPlatformManagedCredentials")
|
||||
.withSchema(TableName.AppConnection)
|
||||
.as("connectionIsPlatformManagedCredentials")
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filters)));
|
||||
}
|
||||
|
||||
if (projectId) {
|
||||
void query.where(`${TableName.Environment}.projectId`, projectId);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
const { offset, limit, sort, count, countDistinct } = options;
|
||||
if (countDistinct) {
|
||||
void query.countDistinct(countDistinct);
|
||||
} else if (count) {
|
||||
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
||||
void query.select("*");
|
||||
}
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRotationV2Query>>[number]>(
|
||||
secretRotation: T,
|
||||
folder: Awaited<ReturnType<TSecretFolderDALFactory["findSecretPathByFolderIds"]>>[number]
|
||||
) => {
|
||||
const {
|
||||
envId,
|
||||
envName,
|
||||
envSlug,
|
||||
connectionApp,
|
||||
connectionName,
|
||||
connectionId,
|
||||
connectionOrgId,
|
||||
connectionEncryptedCredentials,
|
||||
connectionMethod,
|
||||
connectionDescription,
|
||||
connectionCreatedAt,
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionIsPlatformManagedCredentials,
|
||||
...el
|
||||
} = secretRotation;
|
||||
|
||||
return {
|
||||
...el,
|
||||
connectionId,
|
||||
environment: { id: envId, name: envName, slug: envSlug },
|
||||
connection: {
|
||||
app: connectionApp,
|
||||
id: connectionId,
|
||||
name: connectionName,
|
||||
orgId: connectionOrgId,
|
||||
encryptedCredentials: connectionEncryptedCredentials,
|
||||
method: connectionMethod,
|
||||
description: connectionDescription,
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
|
||||
},
|
||||
folder: {
|
||||
id: folder!.id,
|
||||
path: folder!.path
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const secretRotationV2DALFactory = (
|
||||
db: TDbClient,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">
|
||||
) => {
|
||||
const secretRotationV2Orm = ormify(db, TableName.SecretRotationV2);
|
||||
const secretRotationV2SecretMappingOrm = ormify(db, TableName.SecretRotationV2SecretMapping);
|
||||
|
||||
const find = async (
|
||||
filter: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string },
|
||||
options?: TSecretRotationFindOptions,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const secretRotations = await baseSecretRotationV2Query({ filter, db, tx, options });
|
||||
|
||||
if (!secretRotations.length) return [];
|
||||
|
||||
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
|
||||
filter.projectId,
|
||||
secretRotations.map((rotation) => rotation.folderId),
|
||||
tx
|
||||
);
|
||||
|
||||
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
|
||||
|
||||
foldersWithPath.forEach((folder) => {
|
||||
if (folder) folderRecord[folder.id] = folder;
|
||||
});
|
||||
|
||||
return secretRotations.map((rotation) => expandSecretRotation(rotation, folderRecord[rotation.folderId]));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const findWithMappedSecretsCount = async (
|
||||
{
|
||||
search,
|
||||
projectId,
|
||||
...filter
|
||||
}: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string; search?: string },
|
||||
tx?: Knex
|
||||
) => {
|
||||
const query = (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretRotationV2SecretMapping}.rotationId`,
|
||||
`${TableName.SecretRotationV2}.id`
|
||||
)
|
||||
.join(TableName.SecretV2, `${TableName.SecretRotationV2SecretMapping}.secretId`, `${TableName.SecretV2}.id`)
|
||||
.where(`${TableName.Environment}.projectId`, projectId)
|
||||
.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filter)))
|
||||
.countDistinct(`${TableName.SecretRotationV2}.name`);
|
||||
|
||||
if (search) {
|
||||
void query
|
||||
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
|
||||
}
|
||||
|
||||
const result = await query;
|
||||
|
||||
// @ts-expect-error knex infers wrong type...
|
||||
return Number(result[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
const findWithMappedSecrets = async (
|
||||
{ search, ...filter }: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string; search?: string },
|
||||
options?: TSecretRotationFindOptions,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const extendedQuery = baseSecretRotationV2Query({ filter, db, tx, options })
|
||||
.join(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretRotationV2SecretMapping}.rotationId`,
|
||||
`${TableName.SecretRotationV2}.id`
|
||||
)
|
||||
.join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretRotationV2SecretMapping}.secretId`)
|
||||
.leftJoin(
|
||||
TableName.SecretV2JnTag,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretTag,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretV2).as("secretId"),
|
||||
db.ref("key").withSchema(TableName.SecretV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"),
|
||||
db.ref("type").withSchema(TableName.SecretV2).as("secretType"),
|
||||
db.ref("encryptedValue").withSchema(TableName.SecretV2).as("secretEncryptedValue"),
|
||||
db.ref("encryptedComment").withSchema(TableName.SecretV2).as("secretEncryptedComment"),
|
||||
db.ref("reminderNote").withSchema(TableName.SecretV2).as("secretReminderNote"),
|
||||
db.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("secretReminderRepeatDays"),
|
||||
db.ref("skipMultilineEncoding").withSchema(TableName.SecretV2).as("secretSkipMultilineEncoding"),
|
||||
db.ref("metadata").withSchema(TableName.SecretV2).as("secretMetadata"),
|
||||
db.ref("userId").withSchema(TableName.SecretV2).as("secretUserId"),
|
||||
db.ref("folderId").withSchema(TableName.SecretV2).as("secretFolderId"),
|
||||
db.ref("createdAt").withSchema(TableName.SecretV2).as("secretCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.SecretV2).as("secretUpdatedAt"),
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
);
|
||||
|
||||
if (search) {
|
||||
void extendedQuery.where((query) => {
|
||||
void query
|
||||
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
const secretRotations = await extendedQuery;
|
||||
|
||||
if (!secretRotations.length) return [];
|
||||
|
||||
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
|
||||
filter.projectId,
|
||||
secretRotations.map((rotation) => rotation.folderId),
|
||||
tx
|
||||
);
|
||||
|
||||
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
|
||||
|
||||
foldersWithPath.forEach((folder) => {
|
||||
if (folder) folderRecord[folder.id] = folder;
|
||||
});
|
||||
|
||||
return sqlNestRelationships({
|
||||
data: secretRotations,
|
||||
key: "id",
|
||||
parentMapper: (rotation) => expandSecretRotation(rotation, folderRecord[rotation.folderId]),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "secretId",
|
||||
label: "secrets" as const,
|
||||
mapper: ({
|
||||
secretId,
|
||||
secretKey,
|
||||
secretVersion,
|
||||
secretType,
|
||||
secretEncryptedValue,
|
||||
secretEncryptedComment,
|
||||
secretReminderNote,
|
||||
secretReminderRepeatDays,
|
||||
secretSkipMultilineEncoding,
|
||||
secretMetadata,
|
||||
secretUserId,
|
||||
secretFolderId,
|
||||
secretCreatedAt,
|
||||
secretUpdatedAt,
|
||||
id
|
||||
}) => ({
|
||||
id: secretId,
|
||||
key: secretKey,
|
||||
version: secretVersion,
|
||||
type: secretType,
|
||||
encryptedValue: secretEncryptedValue,
|
||||
encryptedComment: secretEncryptedComment,
|
||||
reminderNote: secretReminderNote,
|
||||
reminderRepeatDays: secretReminderRepeatDays,
|
||||
skipMultilineEncoding: secretSkipMultilineEncoding,
|
||||
metadata: secretMetadata,
|
||||
userId: secretUserId,
|
||||
folderId: secretFolderId,
|
||||
createdAt: secretCreatedAt,
|
||||
updatedAt: secretUpdatedAt,
|
||||
rotationId: id,
|
||||
isRotatedSecret: true
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name: slug
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "secretMetadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find with Mapped Secrets - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const secretRotation = await baseSecretRotationV2Query({
|
||||
filter: { id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
|
||||
if (secretRotation) {
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||
secretRotation.projectId,
|
||||
[secretRotation.folderId],
|
||||
tx
|
||||
);
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: Parameters<(typeof secretRotationV2Orm)["create"]>[0], tx?: Knex) => {
|
||||
const rotation = await secretRotationV2Orm.create(data, tx);
|
||||
|
||||
const secretRotation = (await baseSecretRotationV2Query({
|
||||
filter: { id: rotation.id },
|
||||
db,
|
||||
tx
|
||||
}).first())!;
|
||||
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||
secretRotation.projectId,
|
||||
[secretRotation.folderId],
|
||||
tx
|
||||
);
|
||||
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
};
|
||||
|
||||
const updateById = async (
|
||||
rotationId: string,
|
||||
data: Parameters<(typeof secretRotationV2Orm)["updateById"]>[1],
|
||||
tx?: Knex
|
||||
) => {
|
||||
const rotation = await secretRotationV2Orm.updateById(rotationId, data, tx);
|
||||
|
||||
const secretRotation = (await baseSecretRotationV2Query({
|
||||
filter: { id: rotation.id },
|
||||
db,
|
||||
tx
|
||||
}).first())!;
|
||||
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||
secretRotation.projectId,
|
||||
[secretRotation.folderId],
|
||||
tx
|
||||
);
|
||||
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
};
|
||||
|
||||
const deleteById = async (rotationId: string, tx?: Knex) => {
|
||||
const secretRotation = (await baseSecretRotationV2Query({
|
||||
filter: { id: rotationId },
|
||||
db,
|
||||
tx
|
||||
}).first())!;
|
||||
|
||||
await secretRotationV2Orm.deleteById(rotationId, tx);
|
||||
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||
secretRotation.projectId,
|
||||
[secretRotation.folderId],
|
||||
tx
|
||||
);
|
||||
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
};
|
||||
|
||||
const findOne = async (filter: Parameters<(typeof secretRotationV2Orm)["findOne"]>[0], tx?: Knex) => {
|
||||
try {
|
||||
const secretRotation = await baseSecretRotationV2Query({ filter, db, tx }).first();
|
||||
|
||||
if (secretRotation) {
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||
secretRotation.projectId,
|
||||
[secretRotation.folderId],
|
||||
tx
|
||||
);
|
||||
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const findSecretRotationsToQueue = async (rotateBy: Date, tx?: Knex) => {
|
||||
const secretRotations = await (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||
.where(`${TableName.SecretRotationV2}.isAutoRotationEnabled`, true)
|
||||
.whereNotNull(`${TableName.SecretRotationV2}.nextRotationAt`)
|
||||
.andWhereRaw(`"nextRotationAt" <= ?`, [rotateBy])
|
||||
.select(selectAllTableCols(TableName.SecretRotationV2));
|
||||
|
||||
return secretRotations;
|
||||
};
|
||||
|
||||
return {
|
||||
...secretRotationV2Orm,
|
||||
find,
|
||||
create,
|
||||
findById,
|
||||
updateById,
|
||||
deleteById,
|
||||
findOne,
|
||||
insertSecretMappings: secretRotationV2SecretMappingOrm.insertMany,
|
||||
findWithMappedSecrets,
|
||||
findWithMappedSecretsCount,
|
||||
findSecretRotationsToQueue
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
Success = "success",
|
||||
Failed = "failed"
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service";
|
||||
import {
|
||||
TSecretRotationV2,
|
||||
TSecretRotationV2GeneratedCredentials,
|
||||
TSecretRotationV2ListItem,
|
||||
TSecretRotationV2Raw
|
||||
} from "./secret-rotation-v2-types";
|
||||
|
||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
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 now = new Date();
|
||||
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate() + 1, // Add 1 day to get tomorrow
|
||||
hours,
|
||||
minutes,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getNextUTCMinute = ({ minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => {
|
||||
const now = new Date();
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate(),
|
||||
now.getUTCHours(),
|
||||
now.getUTCMinutes() + 1, // Add 1 minute to get the next minute
|
||||
minutes, // use minutes as seconds in dev
|
||||
0
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const getNextUtcRotationInterval = (rotateAtUtc?: TSecretRotationV2["rotateAtUtc"]) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (appCfg.isRotationDevelopmentMode) {
|
||||
return getNextUTCMinute(rotateAtUtc);
|
||||
}
|
||||
|
||||
return getNextUTCMidnight(rotateAtUtc);
|
||||
};
|
||||
|
||||
export const encryptSecretRotationCredentials = async ({
|
||||
projectId,
|
||||
generatedCredentials,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
generatedCredentials: TSecretRotationV2GeneratedCredentials;
|
||||
kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"];
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(generatedCredentials))
|
||||
});
|
||||
|
||||
return encryptedCredentialsBlob;
|
||||
};
|
||||
|
||||
export const decryptSecretRotationCredentials = async ({
|
||||
projectId,
|
||||
encryptedGeneratedCredentials,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
encryptedGeneratedCredentials: Buffer;
|
||||
kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"];
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedGeneratedCredentials
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TSecretRotationV2GeneratedCredentials;
|
||||
};
|
||||
|
||||
export const getSecretRotationRotateSecretJobOptions = ({
|
||||
id,
|
||||
nextRotationAt
|
||||
}: Pick<TSecretRotationV2Raw, "id" | "nextRotationAt">) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
return {
|
||||
jobId: `secret-rotation-v2-rotate-${id}`,
|
||||
retryLimit: appCfg.isRotationDevelopmentMode ? 3 : 5,
|
||||
retryBackoff: true,
|
||||
startAfter: nextRotationAt ?? undefined
|
||||
};
|
||||
};
|
||||
|
||||
export const calculateNextRotationAt = ({
|
||||
rotateAtUtc,
|
||||
isAutoRotationEnabled,
|
||||
rotationInterval,
|
||||
rotationStatus,
|
||||
isManualRotation,
|
||||
...params
|
||||
}: Pick<
|
||||
TSecretRotationV2,
|
||||
"isAutoRotationEnabled" | "lastRotatedAt" | "rotateAtUtc" | "rotationInterval" | "rotationStatus"
|
||||
> & { isManualRotation: boolean }) => {
|
||||
if (!isAutoRotationEnabled) return null;
|
||||
|
||||
if (rotationStatus === SecretRotationStatus.Failed) {
|
||||
return getNextUtcRotationInterval(rotateAtUtc);
|
||||
}
|
||||
|
||||
const lastRotatedAt = new Date(params.lastRotatedAt);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (appCfg.isRotationDevelopmentMode) {
|
||||
// treat interval as minute
|
||||
const nextRotation = new Date(lastRotatedAt.getTime() + rotationInterval * 60 * 1000);
|
||||
|
||||
// in development mode we use rotateAtUtc.minutes as seconds
|
||||
nextRotation.setUTCSeconds(rotateAtUtc.minutes);
|
||||
nextRotation.setUTCMilliseconds(0);
|
||||
|
||||
// If creation/manual rotation seconds are after the configured seconds we pad an additional minute
|
||||
// to ensure a full interval has elapsed before rotation
|
||||
if (isManualRotation && lastRotatedAt.getUTCSeconds() >= rotateAtUtc.minutes) {
|
||||
nextRotation.setUTCMinutes(nextRotation.getUTCMinutes() + 1);
|
||||
}
|
||||
|
||||
return nextRotation;
|
||||
}
|
||||
|
||||
// production mode - rotationInterval = days
|
||||
|
||||
const nextRotation = new Date(lastRotatedAt);
|
||||
|
||||
nextRotation.setUTCHours(rotateAtUtc.hours);
|
||||
nextRotation.setUTCMinutes(rotateAtUtc.minutes);
|
||||
nextRotation.setUTCSeconds(0);
|
||||
nextRotation.setUTCMilliseconds(0);
|
||||
|
||||
// If creation/manual rotation was after the daily rotation time,
|
||||
// we need pad an additional day to ensure full rotation interval
|
||||
if (
|
||||
isManualRotation &&
|
||||
(lastRotatedAt.getUTCHours() > rotateAtUtc.hours ||
|
||||
(lastRotatedAt.getUTCHours() === rotateAtUtc.hours && lastRotatedAt.getUTCMinutes() >= rotateAtUtc.minutes))
|
||||
) {
|
||||
nextRotation.setUTCDate(nextRotation.getUTCDate() + rotationInterval + 1);
|
||||
} else {
|
||||
nextRotation.setUTCDate(nextRotation.getUTCDate() + rotationInterval);
|
||||
}
|
||||
|
||||
return nextRotation;
|
||||
};
|
||||
|
||||
export const expandSecretRotation = async (
|
||||
{ encryptedLastRotationMessage, ...secretRotation }: TSecretRotationV2Raw,
|
||||
kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"]
|
||||
) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
const lastRotationMessage = encryptedLastRotationMessage
|
||||
? decryptor({
|
||||
cipherTextBlob: encryptedLastRotationMessage
|
||||
}).toString()
|
||||
: null;
|
||||
|
||||
return {
|
||||
...secretRotation,
|
||||
lastRotationMessage
|
||||
} as TSecretRotationV2;
|
||||
};
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 1024;
|
||||
|
||||
export const parseRotationErrorMessage = (err: unknown): string => {
|
||||
let errorMessage = `Infisical encountered an issue while generating credentials with the configured inputs: `;
|
||||
|
||||
if (err instanceof AxiosError) {
|
||||
errorMessage += err?.response?.data
|
||||
? JSON.stringify(err?.response?.data)
|
||||
: err?.message ?? "An unknown error occurred.";
|
||||
} else {
|
||||
errorMessage += (err as Error)?.message || "An unknown error occurred.";
|
||||
}
|
||||
|
||||
return errorMessage.length <= MAX_MESSAGE_LENGTH
|
||||
? errorMessage
|
||||
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TSecretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
getNextUtcRotationInterval,
|
||||
getSecretRotationRotateSecretJobOptions
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
|
||||
import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||
import {
|
||||
TSecretRotationRotateSecretsJobPayload,
|
||||
TSecretRotationSendNotificationJobPayload
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
type TSecretRotationV2QueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
secretRotationV2DAL: Pick<TSecretRotationV2DALFactory, "findSecretRotationsToQueue" | "findById">;
|
||||
secretRotationV2Service: Pick<TSecretRotationV2ServiceFactory, "rotateGeneratedCredentials">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export const secretRotationV2QueueServiceFactory = async ({
|
||||
queueService,
|
||||
secretRotationV2DAL,
|
||||
secretRotationV2Service,
|
||||
projectMembershipDAL,
|
||||
projectDAL,
|
||||
smtpService
|
||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (appCfg.isRotationDevelopmentMode) {
|
||||
logger.warn("Secret Rotation V2 is in development mode.");
|
||||
}
|
||||
|
||||
await queueService.startPg<QueueName.SecretRotationV2>(
|
||||
QueueJobs.SecretRotationV2QueueRotations,
|
||||
async () => {
|
||||
try {
|
||||
const rotateBy = getNextUtcRotationInterval();
|
||||
|
||||
const currentTime = new Date();
|
||||
|
||||
const secretRotations = await secretRotationV2DAL.findSecretRotationsToQueue(rotateBy);
|
||||
|
||||
logger.info(
|
||||
`secretRotationV2Queue: Queue Rotations [currentTime=${currentTime.toISOString()}] [rotateBy=${rotateBy.toISOString()}] [count=${
|
||||
secretRotations.length
|
||||
}]`
|
||||
);
|
||||
|
||||
for await (const rotation of secretRotations) {
|
||||
logger.info(
|
||||
`secretRotationV2Queue: Queue Rotation [rotationId=${rotation.id}] [lastRotatedAt=${new Date(
|
||||
rotation.lastRotatedAt
|
||||
).toISOString()}] [rotateAt=${new Date(rotation.nextRotationAt!).toISOString()}]`
|
||||
);
|
||||
await queueService.queuePg(
|
||||
QueueJobs.SecretRotationV2RotateSecrets,
|
||||
{
|
||||
rotationId: rotation.id,
|
||||
queuedAt: currentTime
|
||||
},
|
||||
getSecretRotationRotateSecretJobOptions(rotation)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, "secretRotationV2Queue: Queue Rotations Error:");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
workerCount: 1,
|
||||
pollingIntervalSeconds: 0.5
|
||||
}
|
||||
);
|
||||
|
||||
await queueService.startPg<QueueName.SecretRotationV2>(
|
||||
QueueJobs.SecretRotationV2RotateSecrets,
|
||||
async ([job]) => {
|
||||
const { rotationId, queuedAt, isManualRotation } = job.data as TSecretRotationRotateSecretsJobPayload;
|
||||
const { retryCount, retryLimit } = job;
|
||||
|
||||
const logDetails = `[rotationId=${rotationId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
|
||||
|
||||
try {
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation) throw new Error(`Secret rotation ${rotationId} not found`);
|
||||
|
||||
if (!secretRotation.isAutoRotationEnabled) {
|
||||
logger.info(`secretRotationV2Queue: Skipping Rotation - Auto-Rotation Disabled Since Queue ${logDetails}`);
|
||||
}
|
||||
|
||||
if (new Date(secretRotation.lastRotatedAt).getTime() >= new Date(queuedAt).getTime()) {
|
||||
// rotated since being queued, skip rotation
|
||||
logger.info(`secretRotationV2Queue: Skipping Rotation - Rotated Since Queue ${logDetails}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await secretRotationV2Service.rotateGeneratedCredentials(secretRotation, {
|
||||
jobId: job.id,
|
||||
shouldSendNotification: true,
|
||||
isFinalAttempt: retryCount === retryLimit,
|
||||
isManualRotation
|
||||
});
|
||||
|
||||
logger.info(`secretRotationV2Queue: Secrets Rotated ${logDetails}`);
|
||||
} catch (error) {
|
||||
logger.error(error, `secretRotationV2Queue: Failed to Rotate Secrets ${logDetails}`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
workerCount: 30,
|
||||
pollingIntervalSeconds: 0.5
|
||||
}
|
||||
);
|
||||
|
||||
await queueService.startPg<QueueName.SecretRotationV2>(
|
||||
QueueJobs.SecretRotationV2SendNotification,
|
||||
async ([job]) => {
|
||||
const { secretRotation } = job.data as TSecretRotationSendNotificationJobPayload;
|
||||
try {
|
||||
const {
|
||||
name: rotationName,
|
||||
type,
|
||||
projectId,
|
||||
lastRotationAttemptedAt,
|
||||
folder,
|
||||
environment,
|
||||
id: rotationId
|
||||
} = secretRotation;
|
||||
|
||||
logger.info(`secretRotationV2Queue: Sending Status Notification [rotationId=${rotationId}]`);
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
||||
const projectAdmins = projectMembers.filter((member) =>
|
||||
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
||||
);
|
||||
|
||||
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretRotationFailed,
|
||||
subjectLine: `Secret Rotation Failed`,
|
||||
substitutions: {
|
||||
rotationName,
|
||||
rotationType,
|
||||
content: `Your ${rotationType} Rotation failed to rotate during it's scheduled rotation. The last rotation attempt occurred at ${new Date(
|
||||
lastRotationAttemptedAt
|
||||
).toISOString()}. Please check the rotation status in Infisical for more details.`,
|
||||
secretPath: folder.path,
|
||||
environment: environment.name,
|
||||
projectName: project.name,
|
||||
rotationUrl: encodeURI(`${appCfg.SITE_URL}/secret-manager/${projectId}/secrets/${environment.slug}`)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`secretRotationV2Queue: Failed to Send Status Notification [rotationId=${secretRotation.id}]`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
workerCount: 5,
|
||||
pollingIntervalSeconds: 30
|
||||
}
|
||||
);
|
||||
|
||||
await queueService.schedulePg(
|
||||
QueueJobs.SecretRotationV2QueueRotations,
|
||||
appCfg.isRotationDevelopmentMode ? "* * * * *" : "0 0 * * *",
|
||||
undefined,
|
||||
{ tz: "UTC" }
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationsV2Schema } from "@app/db/schemas/secret-rotations-v2";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
const RotateAtUtcSchema = z.object({
|
||||
hours: z.number().min(0).max(23),
|
||||
minutes: z.number().min(0).max(59)
|
||||
});
|
||||
|
||||
export const BaseSecretRotationSchema = (type: SecretRotation) =>
|
||||
SecretRotationsV2Schema.omit({
|
||||
encryptedGeneratedCredentials: true,
|
||||
encryptedLastRotationMessage: true,
|
||||
rotateAtUtc: true,
|
||||
// unique to provider
|
||||
type: true,
|
||||
parameters: true,
|
||||
secretMappings: true
|
||||
}).extend({
|
||||
connection: z.object({
|
||||
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
||||
name: z.string(),
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }),
|
||||
projectId: z.string(),
|
||||
folder: z.object({ id: z.string(), path: z.string() }),
|
||||
rotateAtUtc: RotateAtUtcSchema,
|
||||
lastRotationMessage: z.string().nullish()
|
||||
});
|
||||
|
||||
export const BaseCreateSecretRotationSchema = (type: SecretRotation) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretRotations.CREATE(type).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.CREATE(type).projectId),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(SecretRotations.CREATE(type).description),
|
||||
connectionId: z.string().uuid().describe(SecretRotations.CREATE(type).connectionId),
|
||||
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.CREATE(type).environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Secret path required")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(SecretRotations.CREATE(type).secretPath),
|
||||
isAutoRotationEnabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(SecretRotations.CREATE(type).isAutoRotationEnabled),
|
||||
rotationInterval: z.coerce.number().min(1).describe(SecretRotations.CREATE(type).rotationInterval),
|
||||
rotateAtUtc: RotateAtUtcSchema.optional().describe(SecretRotations.CREATE(type).rotateAtUtc)
|
||||
});
|
||||
|
||||
export const BaseUpdateSecretRotationSchema = (type: SecretRotation) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretRotations.UPDATE(type).name).optional(),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(SecretRotations.UPDATE(type).description),
|
||||
isAutoRotationEnabled: z.boolean().optional().describe(SecretRotations.UPDATE(type).isAutoRotationEnabled),
|
||||
rotationInterval: z.coerce.number().min(1).optional().describe(SecretRotations.UPDATE(type).rotationInterval),
|
||||
rotateAtUtc: RotateAtUtcSchema.optional().describe(SecretRotations.UPDATE(type).rotateAtUtc)
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import {
|
||||
TMsSqlCredentialsRotation,
|
||||
TMsSqlCredentialsRotationInput,
|
||||
TMsSqlCredentialsRotationListItem,
|
||||
TMsSqlCredentialsRotationWithConnection
|
||||
} from "./mssql-credentials";
|
||||
import {
|
||||
TPostgresCredentialsRotation,
|
||||
TPostgresCredentialsRotationInput,
|
||||
TPostgresCredentialsRotationListItem,
|
||||
TPostgresCredentialsRotationWithConnection
|
||||
} from "./postgres-credentials";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials = TSqlCredentialsRotationGeneratedCredentials;
|
||||
|
||||
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem;
|
||||
|
||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||
|
||||
export type TListSecretRotationsV2ByProjectId = {
|
||||
projectId: string;
|
||||
type?: SecretRotation;
|
||||
};
|
||||
|
||||
export type TFindSecretRotationV2ByIdDTO = {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TRotateSecretRotationV2 = TFindSecretRotationV2ByIdDTO & { auditLogInfo: AuditLogInfo };
|
||||
|
||||
export type TRotateAtUtc = { hours: number; minutes: number };
|
||||
|
||||
export type TFindSecretRotationV2ByNameDTO = {
|
||||
rotationName: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TCreateSecretRotationV2DTO = Pick<
|
||||
TSecretRotationV2,
|
||||
"parameters" | "secretsMapping" | "description" | "rotationInterval" | "name" | "connectionId" | "projectId"
|
||||
> & {
|
||||
type: SecretRotation;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
rotateAtUtc?: TRotateAtUtc;
|
||||
};
|
||||
|
||||
export type TUpdateSecretRotationV2DTO = Partial<
|
||||
Omit<TCreateSecretRotationV2DTO, "projectId" | "connectionId" | "secretPath" | "environment">
|
||||
> & {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TDeleteSecretRotationV2DTO = {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
deleteSecrets: boolean;
|
||||
revokeGeneratedCredentials: boolean;
|
||||
};
|
||||
|
||||
export type TGetDashboardSecretRotationV2Count = {
|
||||
search?: string;
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environments: string[];
|
||||
};
|
||||
|
||||
export type TGetDashboardSecretRotationsV2 = {
|
||||
search?: string;
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environments: string[];
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export type TQuickSearchSecretRotationsV2Filters = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export type TQuickSearchSecretRotationsV2 = {
|
||||
projectId: string;
|
||||
folderMappings: { folderId: string; path: string; environment: string }[];
|
||||
filters: TQuickSearchSecretRotationsV2Filters;
|
||||
};
|
||||
|
||||
export type TSecretRotationRotateGeneratedCredentials = {
|
||||
auditLogInfo?: AuditLogInfo;
|
||||
jobId?: string;
|
||||
shouldSendNotification?: boolean;
|
||||
isFinalAttempt?: boolean;
|
||||
isManualRotation?: boolean;
|
||||
};
|
||||
|
||||
export type TSecretRotationRotateSecretsJobPayload = { rotationId: string; queuedAt: Date; isManualRotation: boolean };
|
||||
|
||||
export type TSecretRotationSendNotificationJobPayload = {
|
||||
secretRotation: TSecretRotationV2Raw;
|
||||
};
|
||||
|
||||
// scott: the reason for the callback structure of the rotation factory is to facilitate, when possible,
|
||||
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
|
||||
// third party credential changes (when supported), preventing credentials getting out of sync
|
||||
|
||||
export type TRotationFactoryIssueCredentials = (
|
||||
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryRevokeCredentials = (
|
||||
generatedCredentials: TSecretRotationV2GeneratedCredentials,
|
||||
callback: () => Promise<TSecretRotationV2Raw>
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryRotateCredentials = (
|
||||
credentialsToRevoke: TSecretRotationV2GeneratedCredentials[number] | undefined,
|
||||
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryGetSecretsPayload = (
|
||||
generatedCredentials: TSecretRotationV2GeneratedCredentials[number]
|
||||
) => { key: string; value: string }[];
|
||||
|
||||
export type TRotationFactory = (secretRotation: TSecretRotationV2WithConnection) => {
|
||||
issueCredentials: TRotationFactoryIssueCredentials;
|
||||
revokeCredentials: TRotationFactoryRevokeCredentials;
|
||||
rotateCredentials: TRotationFactoryRotateCredentials;
|
||||
getSecretsPayload: TRotationFactoryGetSecretsPayload;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
|
||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema
|
||||
]);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./sql-credentials-rotation-fns";
|
||||
export * from "./sql-credentials-rotation-schemas";
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import { randomInt } from "crypto";
|
||||
|
||||
import {
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
|
||||
|
||||
import {
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationWithConnection
|
||||
} from "./sql-credentials-rotation-types";
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
const generatePassword = () => {
|
||||
try {
|
||||
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
const chars = {
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
digits: "0123456789",
|
||||
symbols: allowedSymbols || "-_.~!*"
|
||||
};
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (required.lowercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.lowercase)
|
||||
.fill(0)
|
||||
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.uppercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.uppercase)
|
||||
.fill(0)
|
||||
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.digits > 0) {
|
||||
parts.push(
|
||||
...Array(required.digits)
|
||||
.fill(0)
|
||||
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.symbols > 0) {
|
||||
parts.push(
|
||||
...Array(required.symbols)
|
||||
.fill(0)
|
||||
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||
);
|
||||
}
|
||||
|
||||
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||
|
||||
const allowedChars = Object.entries(chars)
|
||||
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||
.map(([, value]) => value)
|
||||
.join("");
|
||||
|
||||
parts.push(
|
||||
...Array(remainingLength)
|
||||
.fill(0)
|
||||
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||
);
|
||||
|
||||
// shuffle the array to mix up the characters
|
||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||
const j = randomInt(i + 1);
|
||||
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`Failed to generate password: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => {
|
||||
const error = e as Error;
|
||||
|
||||
if (!error?.message) return "Unknown error";
|
||||
|
||||
let redactedMessage = error.message;
|
||||
|
||||
credentials.forEach(({ password }) => {
|
||||
redactedMessage = redactedMessage.replaceAll(password, "*******************");
|
||||
});
|
||||
|
||||
return redactedMessage;
|
||||
};
|
||||
|
||||
export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRotationWithConnection) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { username1, username2 },
|
||||
activeIndex,
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||
const client = await getSqlConnectionClient({
|
||||
...connection,
|
||||
credentials: {
|
||||
...connection.credentials,
|
||||
...credentials
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await client.raw("SELECT 1");
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, [credentials]));
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials = async (callback) => {
|
||||
const client = await getSqlConnectionClient(connection);
|
||||
|
||||
// For SQL, since we get existing users, we change both their passwords
|
||||
// on issue to invalidate their existing passwords
|
||||
const credentialsSet = [
|
||||
{ username: username1, password: generatePassword() },
|
||||
{ username: username2, password: generatePassword() }
|
||||
];
|
||||
|
||||
try {
|
||||
await client.transaction(async (tx) => {
|
||||
for await (const credentials of credentialsSet) {
|
||||
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, credentialsSet));
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
for await (const credentials of credentialsSet) {
|
||||
await validateCredentials(credentials);
|
||||
}
|
||||
|
||||
return callback(credentialsSet[0]);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials = async (credentialsToRevoke, callback) => {
|
||||
const client = await getSqlConnectionClient(connection);
|
||||
|
||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
||||
|
||||
try {
|
||||
await client.transaction(async (tx) => {
|
||||
for await (const credentials of revokedCredentials) {
|
||||
// invalidate previous passwords
|
||||
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, revokedCredentials));
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials = async (_, callback) => {
|
||||
const client = await getSqlConnectionClient(connection);
|
||||
|
||||
// generate new password for the next active user
|
||||
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
|
||||
|
||||
try {
|
||||
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, [credentials]));
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
await validateCredentials(credentials);
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload = (generatedCredentials) => {
|
||||
const { username, password } = secretsMapping;
|
||||
|
||||
const secrets = [
|
||||
{
|
||||
key: username,
|
||||
value: generatedCredentials.username
|
||||
},
|
||||
{
|
||||
key: password,
|
||||
value: generatedCredentials.password
|
||||
}
|
||||
];
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload,
|
||||
validateCredentials
|
||||
};
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
|
||||
export const SqlCredentialsRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
export const SqlCredentialsRotationParametersSchema = z.object({
|
||||
username1: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username1 Required")
|
||||
.describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username1),
|
||||
username2: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username2 Required")
|
||||
.describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username2)
|
||||
});
|
||||
|
||||
export const SqlCredentialsRotationSecretsMappingSchema = z.object({
|
||||
username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.SQL_CREDENTIALS.username),
|
||||
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.SQL_CREDENTIALS.password)
|
||||
});
|
||||
|
||||
export const SqlCredentialsRotationTemplateSchema = z.object({
|
||||
createUserStatement: z.string(),
|
||||
secretsMapping: z.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TMsSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { TPostgresCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
|
||||
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "./sql-credentials-rotation-schemas";
|
||||
|
||||
export type TSqlCredentialsRotationWithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection;
|
||||
|
||||
export type TSqlCredentialsRotationGeneratedCredentials = z.infer<
|
||||
typeof SqlCredentialsRotationGeneratedCredentialsSchema
|
||||
>;
|
||||
Reference in New Issue
Block a user