Merge branch 'heads/main' into daniel/kms-sign-verify

This commit is contained in:
Daniel Hougaard
2025-04-10 01:12:13 +04:00
423 changed files with 15652 additions and 1916 deletions
@@ -2,6 +2,13 @@ import {
TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
TCreateSecretRotationV2DTO,
TDeleteSecretRotationV2DTO,
TSecretRotationV2Raw,
TUpdateSecretRotationV2DTO
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
@@ -57,6 +64,8 @@ export type TCreateAuditLogDTO = {
projectId?: string;
} & BaseAuthData;
export type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
interface BaseAuthData {
ipAddress?: string;
userAgent?: string;
@@ -291,7 +300,17 @@ export enum EventType {
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
KMIP_OPERATION_REGISTER = "kmip-operation-register"
KMIP_OPERATION_REGISTER = "kmip-operation-register",
GET_SECRET_ROTATIONS = "get-secret-rotations",
GET_SECRET_ROTATION = "get-secret-rotation",
GET_SECRET_ROTATION_GENERATED_CREDENTIALS = "get-secret-rotation-generated-credentials",
CREATE_SECRET_ROTATION = "create-secret-rotation",
UPDATE_SECRET_ROTATION = "update-secret-rotation",
DELETE_SECRET_ROTATION = "delete-secret-rotation",
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
PROJECT_ACCESS_REQUEST = "project-access-request"
}
export const filterableSecretEvents: EventType[] = [
@@ -2316,6 +2335,15 @@ interface KmipOperationRegisterEvent {
};
}
interface ProjectAccessRequestEvent {
type: EventType.PROJECT_ACCESS_REQUEST;
metadata: {
projectId: string;
requesterId: string;
requesterEmail: string;
};
}
interface SetupKmipEvent {
type: EventType.SETUP_KMIP;
metadata: {
@@ -2341,6 +2369,63 @@ interface RegisterKmipServerEvent {
};
}
interface GetSecretRotationsEvent {
type: EventType.GET_SECRET_ROTATIONS;
metadata: {
type?: SecretRotation;
count: number;
rotationIds: string[];
secretPath?: string;
environment?: string;
};
}
interface GetSecretRotationEvent {
type: EventType.GET_SECRET_ROTATION;
metadata: {
type: SecretRotation;
rotationId: string;
secretPath: string;
environment: string;
};
}
interface GetSecretRotationCredentialsEvent {
type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS;
metadata: {
type: SecretRotation;
rotationId: string;
secretPath: string;
environment: string;
};
}
interface CreateSecretRotationEvent {
type: EventType.CREATE_SECRET_ROTATION;
metadata: Omit<TCreateSecretRotationV2DTO, "projectId"> & { rotationId: string };
}
interface UpdateSecretRotationEvent {
type: EventType.UPDATE_SECRET_ROTATION;
metadata: TUpdateSecretRotationV2DTO;
}
interface DeleteSecretRotationEvent {
type: EventType.DELETE_SECRET_ROTATION;
metadata: TDeleteSecretRotationV2DTO;
}
interface RotateSecretRotationEvent {
type: EventType.SECRET_ROTATION_ROTATE_SECRETS;
metadata: Pick<TSecretRotationV2Raw, "parameters" | "secretsMapping" | "type" | "connectionId" | "folderId"> & {
status: SecretRotationStatus;
rotationId: string;
jobId?: string | undefined;
occurredAt: Date;
message?: string | null | undefined;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -2554,5 +2639,13 @@ export type Event =
| KmipOperationRevokeEvent
| KmipOperationLocateEvent
| KmipOperationRegisterEvent
| ProjectAccessRequestEvent
| CreateSecretRequestEvent
| SecretApprovalRequestReview;
| SecretApprovalRequestReview
| GetSecretRotationsEvent
| GetSecretRotationEvent
| GetSecretRotationCredentialsEvent
| CreateSecretRotationEvent
| UpdateSecretRotationEvent
| DeleteSecretRotationEvent
| RotateSecretRotationEvent;
@@ -8,11 +8,13 @@ import { getDbConnectionHost } from "@app/lib/knex";
export const verifyHostInputValidity = async (host: string, isGateway = false) => {
const appCfg = getConfig();
// if (appCfg.NODE_ENV === "development") return ["host.docker.internal"]; // incase you want to remove this check in dev
if (appCfg.isDevelopmentMode) return [host];
const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat(
(appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)),
getDbConnectionHost(appCfg.REDIS_URL)
getDbConnectionHost(appCfg.REDIS_URL),
getDbConnectionHost(appCfg.AUDIT_LOGS_DB_CONNECTION_URI)
);
// get host db ip
@@ -40,7 +42,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
inputHostIps.push(...resolvedIps);
}
if (!isGateway) {
if (!isGateway && !appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP) {
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
}
@@ -39,7 +39,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretRotation: true,
secretRotation: false,
caCrl: false,
instanceUserManagement: false,
externalKms: false,
@@ -5,6 +5,7 @@
// TODO(akhilmhdh): With tony find out the api structure and fill it here
import { ForbiddenError } from "@casl/ability";
import { CronJob } from "cron";
import { Knex } from "knex";
import { TKeyStoreFactory } from "@app/keystore/keystore";
@@ -85,6 +86,20 @@ export const licenseServiceFactory = ({
appCfg.LICENSE_KEY || ""
);
const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => {
logger.info("Start syncing license key features");
try {
const {
data: { currentPlan }
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
onPremFeatures = currentPlan;
logger.info("Successfully synchronized license key features");
} catch (error) {
logger.error(error, "Failed to synchronize license key features");
if (shouldThrow) throw error;
}
};
const init = async () => {
try {
if (appCfg.LICENSE_SERVER_KEY) {
@@ -98,10 +113,7 @@ export const licenseServiceFactory = ({
if (appCfg.LICENSE_KEY) {
const token = await licenseServerOnPremApi.refreshLicense();
if (token) {
const {
data: { currentPlan }
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
onPremFeatures = currentPlan;
await syncLicenseKeyOnPremFeatures(true);
instanceType = InstanceType.EnterpriseOnPrem;
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
isValidLicense = true;
@@ -147,6 +159,15 @@ export const licenseServiceFactory = ({
}
};
const initializeBackgroundSync = async () => {
if (appCfg.LICENSE_KEY) {
logger.info("Setting up background sync process for refresh onPremFeatures");
const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures);
job.start();
return job;
}
};
const getPlan = async (orgId: string, projectId?: string) => {
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
try {
@@ -662,6 +683,7 @@ export const licenseServiceFactory = ({
getOrgTaxInvoices,
getOrgTaxIds,
addOrgTaxId,
delOrgTaxId
delOrgTaxId,
initializeBackgroundSync
};
};
@@ -56,7 +56,7 @@ export type TFeatureSet = {
trial_end: null;
has_used_trial: true;
secretApproval: false;
secretRotation: true;
secretRotation: false;
caCrl: false;
instanceUserManagement: false;
externalKms: false;
@@ -79,6 +79,15 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets"
}
export enum ProjectPermissionSecretRotationActions {
Read = "read",
ReadGeneratedCredentials = "read-generated-credentials",
Create = "create",
Edit = "edit",
Delete = "delete",
RotateSecrets = "rotate-secrets"
}
export enum ProjectPermissionKmipActions {
CreateClients = "create-clients",
UpdateClients = "update-clients",
@@ -144,6 +153,11 @@ export type SecretImportSubjectFields = {
secretPath: string;
};
export type SecretRotationsSubjectFields = {
environment: string;
secretPath: string;
};
export type IdentityManagementSubjectFields = {
identityId: string;
};
@@ -186,7 +200,13 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [
ProjectPermissionSecretRotationActions,
(
| ProjectPermissionSub.SecretRotation
| (ForcedSubject<ProjectPermissionSub.SecretRotation> & SecretRotationsSubjectFields)
)
]
| [
ProjectPermissionIdentityActions,
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
@@ -302,12 +322,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
@@ -489,6 +503,12 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema
]);
@@ -543,6 +563,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretRotationActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV1Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema
]);
@@ -556,7 +586,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
@@ -682,6 +711,18 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Kmip
);
can(
[
ProjectPermissionSecretRotationActions.Create,
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
ProjectPermissionSecretRotationActions.RotateSecrets
],
ProjectPermissionSub.SecretRotation
);
return rules;
};
@@ -731,7 +772,7 @@ const buildMemberPermissionRules = () => {
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
@@ -879,7 +920,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
@@ -257,6 +257,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema("secVerTag")
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretV2}.id`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({
secVerTagId: "secVerTag.id",
@@ -285,7 +290,8 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
)
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
const formatedDoc = sqlNestRelationships({
data: doc,
key: "id",
@@ -304,14 +310,16 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{
key: "secretId",
label: "secret" as const,
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) =>
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId, rotationId }) =>
secretId
? {
id: secretId,
version: orgSecVersion,
key: orgSecKey,
encryptedValue: orgSecValue,
encryptedComment: orgSecComment
encryptedComment: orgSecComment,
isRotatedSecret: Boolean(rotationId),
rotationId
}
: undefined
},
@@ -262,7 +262,13 @@ export const secretApprovalRequestServiceFactory = ({
id: el.id,
version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
isRotatedSecret: el.secret.isRotatedSecret,
// eslint-disable-next-line no-nested-ternary
secretValue: el.secret.isRotatedSecret
? undefined
: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: "",
@@ -609,7 +615,7 @@ export const secretApprovalRequestServiceFactory = ({
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
typeof el.encryptedValue !== "undefined"
!el.secret.isRotatedSecret && typeof el.encryptedValue !== "undefined"
? {
encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue
@@ -0,0 +1,3 @@
export * from "./mssql-credentials-rotation-constants";
export * from "./mssql-credentials-rotation-schemas";
export * from "./mssql-credentials-rotation-types";
@@ -0,0 +1,29 @@
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 at the server level
CREATE LOGIN [infisical_user] WITH PASSWORD = 'my-password';
-- Grant server-level connect permission
GRANT CONNECT SQL TO [infisical_user];
-- Switch to the database where you want to create the user
USE my_database;
-- Create the database user mapped to the login
CREATE USER [infisical_user] FOR LOGIN [infisical_user];
-- Grant permissions to the user on the schema in this database
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [infisical_user];`,
secretsMapping: {
username: "MSSQL_DB_USERNAME",
password: "MSSQL_DB_PASSWORD"
}
}
};
@@ -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
});
@@ -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";
@@ -0,0 +1,23 @@
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 role
CREATE USER infisical_user WITH ENCRYPTED PASSWORD 'temporary_password';
-- grant database connection permissions
GRANT CONNECT ON DATABASE my_database TO infisical_user;
-- grant relevant table permissions
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO infisical_user;`,
secretsMapping: {
username: "POSTGRES_DB_USERNAME",
password: "POSTGRES_DB_PASSWORD"
}
}
};
@@ -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
});
@@ -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,467 @@
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.where((qb) => {
void qb
.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 getNextUTCDayInterval = ({ 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 getNextUTCMinuteInterval = ({ 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 getNextUTCMinuteInterval(rotateAtUtc);
}
return getNextUTCDayInterval(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: appCfg.isRotationDevelopmentMode ? 0.5 : 30
}
);
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: 2,
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: 2,
pollingIntervalSeconds: 1
}
);
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";
@@ -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
};
};
@@ -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()
})
});
@@ -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
>;
@@ -16,8 +16,8 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
@@ -69,7 +69,10 @@ export const secretRotationServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSub.SecretRotation
);
return {
custom: [],
@@ -99,7 +102,7 @@ export const secretRotationServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSub.SecretRotation
);
@@ -208,7 +211,10 @@ export const secretRotationServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSub.SecretRotation
);
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const docs = await secretRotationDAL.findSecretV2({ projectId });
@@ -254,7 +260,10 @@ export const secretRotationServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSub.SecretRotation
);
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
return doc;
@@ -273,7 +282,7 @@ export const secretRotationServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSub.SecretRotation
);
const deletedDoc = await secretRotationDAL.transaction(async (tx) => {
@@ -398,8 +398,32 @@ export const secretSnapshotServiceFactory = ({
if (shouldUseBridge) {
const rollback = await snapshotDAL.transaction(async (tx) => {
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
// this will remove all secrets in current folder
const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx);
const secretRotationIds = rollbackSnaps
.flatMap((snap) => snap.secretVersions)
.filter((el) => el.isRotatedSecret)
.map((el) => el.secretId);
// this will remove all secrets in current folder except rotated secrets which we ignore
const deletedTopLevelSecs = await secretV2BridgeDAL.delete(
{
$complex: {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: snapshot.folderId
},
{
operator: "notIn",
field: "id",
value: secretRotationIds
}
]
}
},
tx
);
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
// this will remove all secrets and folders on child
// due to sql foreign key and link list connection removing the folders removes everything below too
@@ -424,28 +448,31 @@ export const secretSnapshotServiceFactory = ({
);
const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map(
({
latestSecretVersion,
version,
updatedAt,
createdAt,
secretId,
envId,
id,
tags,
// exclude the bottom fields from the secret - they are for versioning only.
userActorId,
identityActorId,
actorType,
...el
}) => ({
...el,
id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
folderId
})
)
secretVersions
.filter((v) => !v.isRotatedSecret)
.map(
({
latestSecretVersion,
version,
updatedAt,
createdAt,
secretId,
envId,
id,
tags,
// exclude the bottom fields from the secret - they are for versioning only.
userActorId,
identityActorId,
actorType,
isRotatedSecret,
...el
}) => ({
...el,
id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
folderId
})
)
),
tx
);
@@ -181,6 +181,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretRotationV2SecretMapping}.secretId`,
`${TableName.SecretVersionV2}.secretId`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
@@ -195,7 +200,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)
);
return sqlNestRelationships({
data,
@@ -221,7 +227,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => SecretVersionsV2Schema.parse(el),
mapper: (el) => ({
...SecretVersionsV2Schema.parse(el),
isRotatedSecret: Boolean(el.rotationId),
rotationId: el.rotationId
}),
childrenMapper: [
{
key: "tagVersionId",
@@ -476,6 +486,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretVersionV2}.secretId`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.leftJoin<{ latestSecretVersion: number }>(
(tx || db)(TableName.SecretVersionV2)
.groupBy("secretId")
@@ -506,7 +521,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)
);
const formated = sqlNestRelationships({
@@ -523,7 +539,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
label: "secretVersions" as const,
mapper: (el) => ({
...SecretVersionsV2Schema.parse(el),
latestSecretVersion: el.latestSecretVersion as number
latestSecretVersion: el.latestSecretVersion as number,
isRotatedSecret: Boolean(el.rotationId)
}),
childrenMapper: [
{