feat: changed backend-pg to backend

This commit is contained in:
Akhil Mohan
2024-01-27 19:14:33 +05:30
parent d13eafcef7
commit 66d258f02b
792 changed files with 40298 additions and 40607 deletions
@@ -0,0 +1,138 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { SecretRotationsSchema, TableName, TSecretRotations } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
export type TSecretRotationDALFactory = ReturnType<typeof secretRotationDALFactory>;
export const secretRotationDALFactory = (db: TDbClient) => {
const secretRotationOrm = ormify(db, TableName.SecretRotation);
const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput);
const findQuery = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
tx(TableName.SecretRotation)
.where(filter)
.join(
TableName.Environment,
`${TableName.SecretRotation}.envId`,
`${TableName.Environment}.id`
)
.leftJoin(
TableName.SecretRotationOutput,
`${TableName.SecretRotation}.id`,
`${TableName.SecretRotationOutput}.rotationId`
)
.join(
TableName.Secret,
`${TableName.SecretRotationOutput}.secretId`,
`${TableName.Secret}.id`
)
.select(selectAllTableCols(TableName.SecretRotation))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(tx.ref("key").withSchema(TableName.SecretRotationOutput).as("outputKey"))
.select(tx.ref("id").withSchema(TableName.Secret).as("secId"))
.select(tx.ref("version").withSchema(TableName.Secret).as("secVersion"))
.select(tx.ref("secretKeyIV").withSchema(TableName.Secret))
.select(tx.ref("secretKeyTag").withSchema(TableName.Secret))
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret))
.select(tx.ref("secretValueIV").withSchema(TableName.Secret))
.select(tx.ref("secretValueTag").withSchema(TableName.Secret))
.select(tx.ref("secretValueCiphertext").withSchema(TableName.Secret))
.select(tx.ref("secretCommentIV").withSchema(TableName.Secret))
.select(tx.ref("secretCommentTag").withSchema(TableName.Secret))
.select(tx.ref("secretCommentCiphertext").withSchema(TableName.Secret));
const find = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
try {
const data = await findQuery(filter, tx || db);
return sqlNestRelationships({
data,
key: "id",
parentMapper: (el) => ({
...SecretRotationsSchema.parse(el),
projectId: el.projectId,
environment: { id: el.envId, name: el.envName, slug: el.envSlug }
}),
childrenMapper: [
{
key: "secId",
label: "outputs" as const,
mapper: ({
secId,
outputKey,
secVersion,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretValueTag,
secretValueIV,
secretValueCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext
}) => ({
key: outputKey,
secret: {
id: secId,
version: secVersion,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretValueTag,
secretValueIV,
secretValueCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext
}
})
}
]
});
} catch (error) {
throw new DatabaseError({ error, name: "SecretRotationFind" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.SecretRotation)
.join(
TableName.Environment,
`${TableName.SecretRotation}.envId`,
`${TableName.Environment}.id`
)
.where({ [`${TableName.SecretRotation}.id` as "id"]: id })
.select(selectAllTableCols(TableName.SecretRotation))
.select(
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
)
.first();
if (doc) {
const { envName, envSlug, envId, ...el } = doc;
return { ...el, envId, environment: { id: envId, slug: envSlug, name: envName } };
}
} catch (error) {
throw new DatabaseError({ error, name: "SecretRotationFindById" });
}
};
const findRotationOutputsByRotationId = async (rotationId: string) =>
secretRotationOutputOrm.find({ rotationId });
return {
...secretRotationOrm,
find,
findById,
secretOutputInsertMany: secretRotationOutputOrm.insertMany,
findRotationOutputsByRotationId
};
};
@@ -0,0 +1,2 @@
export type { TSecretRotationQueueFactory } from "./secret-rotation-queue";
export { DisableRotationErrors, secretRotationQueueFactory } from "./secret-rotation-queue";
@@ -0,0 +1,158 @@
/* eslint-disable no-param-reassign */
import axios from "axios";
import jmespath from "jmespath";
import knex from "knex";
import { getConfig } from "@app/lib/config/env";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import {
TAssignOp,
TDbProviderClients,
TDirectAssignOp,
THttpProviderFunction
} from "../templates/types";
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
const REGEX = /\${([^}]+)}/g;
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
if (!data) return;
if (typeof data === "number") return data;
if (typeof data === "string") {
return data.replace(REGEX, (_a, b) => getValue(b) as string);
}
if (typeof data === "object" && Array.isArray(data)) {
data.forEach((el, index) => {
// eslint-disable-next-line
data[index] = interpolate(el, getValue);
});
}
if (typeof data === "object") {
if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref);
const temp = data as Record<string, unknown>; // for converting ts object to record type
Object.keys(temp).forEach((key) => {
temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue);
});
}
return data;
};
const getInterpolationValue = (variables: TSecretRotationData) => (key: string) => {
if (key.includes("|")) {
const [keyword, ...arg] = key.split("|").map((el) => el.trim());
switch (keyword) {
case "random": {
return alphaNumericNanoId(parseInt(arg[0], 10));
}
default: {
throw Error(`Interpolation key not found - ${key}`);
}
}
}
const [type, keyName] = key.split(".").map((el) => el.trim());
return variables[type as keyof TSecretRotationData][keyName];
};
export const secretRotationHttpFn = async (
func: THttpProviderFunction,
variables: TSecretRotationData
) => {
// string interpolation
const headers = interpolate(func.header, getInterpolationValue(variables));
const url = interpolate(func.url, getInterpolationValue(variables));
const body = interpolate(func.body, getInterpolationValue(variables));
// axios will automatically throw error if req status is not between 2xx range
return axios({
method: func.method,
url,
headers,
data: body,
timeout: EXTERNAL_REQUEST_TIMEOUT,
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT)
});
};
export const secretRotationDbFn = async ({
ca,
host,
port,
query,
database,
password,
username,
client,
variables
}: TSecretRotationDbFn) => {
const appCfg = getConfig();
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host))
throw new Error("Invalid db host");
const db = knex({
client,
connection: {
database,
port,
host,
user: username,
password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl,
pool: { min: 0, max: 1 }
}
});
const data = await db.raw(query, variables);
return data;
};
export const secretRotationPreSetFn = (
op: Record<string, TDirectAssignOp>,
variables: TSecretRotationData
) => {
const getValFn = getInterpolationValue(variables);
Object.entries(op || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof TSecretRotationData, string];
variables[type][keyName] = interpolate(assignFn.value, getValFn);
});
};
export const secretRotationHttpSetFn = async (
func: THttpProviderFunction,
variables: TSecretRotationData
) => {
const getValFn = getInterpolationValue(variables);
// http setter
const res = await secretRotationHttpFn(func, variables);
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof TSecretRotationData, string];
if (assignFn.assign === TAssignOp.JmesPath) {
variables[type][keyName] = jmespath.search(res.data, assignFn.path);
} else if (assignFn.value) {
variables[type][keyName] = interpolate(assignFn.value, getValFn);
}
});
};
export const getDbSetQuery = (
db: TDbProviderClients,
variables: { username: string; password: string }
) => {
if (db === TDbProviderClients.Pg) {
return {
query: `ALTER USER ?? WITH PASSWORD '${variables.password}'`,
variables: [variables.username]
};
}
// add more based on client
return {
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,
variables: [variables.username]
};
};
@@ -0,0 +1,27 @@
import { TDbProviderClients } from "../templates/types";
export type TSecretRotationEncData = {
inputs: Record<string, unknown>;
creds: Array<{
outputs: Record<string, unknown>;
internal: Record<string, unknown>;
}>;
};
export type TSecretRotationData = {
inputs: Record<string, unknown>;
outputs: Record<string, unknown>;
internal: Record<string, unknown>;
};
export type TSecretRotationDbFn = {
client: TDbProviderClients;
username: string;
password: string;
host: string;
database: string;
port: number;
query: string;
variables: unknown[];
ca?: string;
};
@@ -0,0 +1,302 @@
import { SecretKeyEncoding, SecretType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import {
encryptSymmetric128BitHexKeyUTF8,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { TSecretRotationDALFactory } from "../secret-rotation-dal";
import { rotationTemplates } from "../templates";
import {
TDbProviderClients,
TProviderFunctionTypes,
TSecretRotationProviderTemplate
} from "../templates/types";
import {
getDbSetQuery,
secretRotationDbFn,
secretRotationHttpFn,
secretRotationHttpSetFn,
secretRotationPreSetFn
} from "./secret-rotation-queue-fn";
import {
TSecretRotationData,
TSecretRotationDbFn,
TSecretRotationEncData
} from "./secret-rotation-queue-types";
export type TSecretRotationQueueFactory = ReturnType<typeof secretRotationQueueFactory>;
type TSecretRotationQueueFactoryDep = {
queue: TQueueServiceFactory;
secretRotationDAL: TSecretRotationDALFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">;
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
};
// These error should stop the repeatable job and ask user to reconfigure rotation
export class DisableRotationErrors extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message: string; name?: string; error?: unknown }) {
super(message);
this.name = name || "DisableRotationErrors";
this.error = error;
}
}
export const secretRotationQueueFactory = ({
queue,
secretRotationDAL,
projectBotService,
secretDAL,
secretVersionDAL,
telemetryService
}: TSecretRotationQueueFactoryDep) => {
const addToQueue = async (rotationId: string, interval: number) => {
const appCfg = getConfig();
queue.queue(
QueueName.SecretRotation,
QueueJobs.SecretRotation,
{ rotationId },
{
jobId: rotationId,
repeat: {
// on prod it this will be in days, in development this will be second
every:
appCfg.NODE_ENV === "development"
? secondsToMillis(interval)
: daysToMillisecond(interval),
immediately: true
}
}
);
};
const removeFromQueue = async (rotationId: string, interval: number) => {
const appCfg = getConfig();
await queue.stopRepeatableJob(
QueueName.SecretRotation,
QueueJobs.SecretRotation,
{
// on prod it this will be in days, in development this will be second
every:
appCfg.NODE_ENV === "development"
? secondsToMillis(interval)
: daysToMillisecond(interval)
},
rotationId
);
};
queue.start(QueueName.SecretRotation, async (job) => {
const { rotationId } = job.data;
logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`);
const secretRotation = await secretRotationDAL.findById(rotationId);
const rotationProvider = rotationTemplates.find(
({ name }) => name === secretRotation?.provider
);
try {
if (!rotationProvider || !secretRotation)
throw new DisableRotationErrors({ message: "Provider not found" });
const rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
if (!rotationOutputs.length)
throw new DisableRotationErrors({ message: "Secrets not found" });
// deep copy
const provider = JSON.parse(
JSON.stringify(rotationProvider)
) as TSecretRotationProviderTemplate;
// now get the encrypted variable values
// in includes the inputs, the previous outputs
// internal mapping variables etc
const { encryptedDataTag, encryptedDataIV, encryptedData, keyEncoding } = secretRotation;
if (!encryptedDataTag || !encryptedDataIV || !encryptedData || !keyEncoding) {
throw new DisableRotationErrors({ message: "No inputs found" });
}
const decryptedData = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: encryptedData,
iv: encryptedDataIV,
tag: encryptedDataTag
});
const variables = JSON.parse(decryptedData) as TSecretRotationEncData;
// rotation set cycle
const newCredential: TSecretRotationData = {
inputs: variables.inputs,
outputs: {},
internal: {}
};
// when its a database we keep cycling the variables accordingly
if (provider.template.type === TProviderFunctionTypes.DB) {
const lastCred = variables.creds.at(-1);
if (lastCred && variables.creds.length === 1) {
newCredential.internal.username =
lastCred.internal.username === variables.inputs.username1
? variables.inputs.username2
: variables.inputs.username1;
} else {
newCredential.internal.username = lastCred
? lastCred.internal.username
: variables.inputs.username1;
}
// set a random value for new password
newCredential.internal.rotated_password = alphaNumericNanoId(32);
const {
admin_username: username,
admin_password: password,
host,
database,
port,
ca
} = newCredential.inputs;
const dbFunctionArg = {
username,
password,
host,
database,
port,
ca: ca as string,
client:
provider.template.client === TDbProviderClients.MySql
? "mysql2"
: provider.template.client
} as TSecretRotationDbFn;
// set function
await secretRotationDbFn({
...dbFunctionArg,
...getDbSetQuery(provider.template.client, {
password: newCredential.internal.rotated_password as string,
username: newCredential.internal.username as string
})
});
// test function
await secretRotationDbFn({
...dbFunctionArg,
query: "SELECT NOW()",
variables: []
});
newCredential.outputs.db_username = newCredential.internal.username;
newCredential.outputs.db_password = newCredential.internal.rotated_password;
// clean up
if (variables.creds.length === 2) variables.creds.pop();
}
if (provider.template.type === TProviderFunctionTypes.HTTP) {
if (provider.template.functions.set?.pre) {
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
}
await secretRotationHttpSetFn(provider.template.functions.set, newCredential);
// now test
await secretRotationHttpFn(provider.template.functions.test, newCredential);
if (variables.creds.length === 2) {
const deleteCycleCred = variables.creds.pop();
if (deleteCycleCred && provider.template.functions.remove) {
const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred };
await secretRotationHttpFn(provider.template.functions.remove, deleteCycleVar);
}
}
}
variables.creds.unshift({
outputs: newCredential.outputs,
internal: newCredential.internal
});
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables));
const key = await projectBotService.getBotKey(secretRotation.projectId);
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
secretId,
value: encryptSymmetric128BitHexKeyUTF8(
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey]),
key
)
}));
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
},
tx
);
const updatedSecrets = await secretDAL.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({
// this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag
}
})),
tx
);
await secretVersionDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el,
secretId: id
})),
tx
);
});
telemetryService.sendPostHogEvents({
event: PostHogEventTypes.SecretRotated,
distinctId: "",
properties: {
numberOfSecrets: encryptedSecrets.length,
environment: secretRotation.environment.slug,
secretPath: secretRotation.secretPath,
workspaceId: secretRotation.projectId
}
});
logger.info("Finished rotating: rotation id: ", rotationId);
} catch (error) {
logger.error(error);
if (error instanceof DisableRotationErrors) {
if (job.id) {
queue.stopRepeatableJobByJobId(QueueName.SecretRotation, job.id);
}
}
await secretRotationDAL.updateById(rotationId, {
status: "failed",
statusMessage: (error as Error).message.slice(0, 500),
lastRotatedAt: new Date()
});
}
});
return {
addToQueue,
removeFromQueue
};
};
@@ -0,0 +1,232 @@
import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
import {
TCreateSecretRotationDTO,
TDeleteDTO,
TGetByIdDTO,
TListByProjectIdDTO,
TRestartDTO
} from "./secret-rotation-types";
import { rotationTemplates } from "./templates";
type TSecretRotationServiceFactoryDep = {
secretRotationDAL: TSecretRotationDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
secretDAL: Pick<TSecretDALFactory, "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory;
};
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
const ajv = new Ajv({ strict: false });
export const secretRotationServiceFactory = ({
secretRotationDAL,
permissionService,
secretRotationQueue,
licenseService,
projectDAL,
folderDAL,
secretDAL
}: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation
);
return {
custom: [],
providers: rotationTemplates
};
};
const createRotation = async ({
projectId,
actorId,
actor,
inputs,
outputs,
interval,
provider,
secretPath,
environment
}: TCreateSecretRotationDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Secret path not found" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
const project = await projectDAL.findById(projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message:
"Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
const selectedTemplate = rotationTemplates.find(({ name }) => name === provider);
if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" });
const formattedInputs: Record<string, unknown> = {};
Object.entries(inputs).forEach(([key, value]) => {
const { type } = selectedTemplate.template.inputs.properties[key];
if (type === "string") {
formattedInputs[key] = value;
return;
}
if (type === "integer") {
formattedInputs[key] = parseInt(value as string, 10);
return;
}
formattedInputs[key] = JSON.parse(value as string);
});
// ensure input one follows the correct schema
const valid = ajv.validate(selectedTemplate.template.inputs, formattedInputs);
if (!valid) {
throw new BadRequestError({ message: ajv.errors?.[0].message });
}
const unencryptedData: Partial<TSecretRotationEncData> = {
inputs: formattedInputs,
creds: []
};
const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData));
const secretRotation = secretRotationDAL.transaction(async (tx) => {
const doc = await secretRotationDAL.create(
{
provider,
secretPath,
interval,
envId: folder.envId,
encryptedDataTag: encData.tag,
encryptedDataIV: encData.iv,
encryptedData: encData.ciphertext,
algorithm: encData.algorithm,
keyEncoding: encData.encoding
},
tx
);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
const outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
});
return secretRotation;
};
const getById = async ({ rotationId, actor, actorId }: TGetByIdDTO) => {
const [doc] = await secretRotationDAL.find({ id: rotationId });
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
doc.projectId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation
);
return doc;
};
const getByProjectId = async ({ actorId, projectId, actor }: TListByProjectIdDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation
);
const doc = await secretRotationDAL.find({ projectId });
return doc;
};
const restartById = async ({ actor, actorId, rotationId }: TRestartDTO) => {
const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
const project = await projectDAL.findById(doc.projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message:
"Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
doc.projectId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SecretRotation
);
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
return doc;
};
const deleteById = async ({ actor, actorId, rotationId }: TDeleteDTO) => {
const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
doc.projectId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation
);
const deletedDoc = await secretRotationDAL.transaction(async (tx) => {
const strat = await secretRotationDAL.deleteById(rotationId, tx);
await secretRotationQueue.removeFromQueue(strat.id, strat.interval);
return strat;
});
return { ...doc, ...deletedDoc };
};
return {
getProviderTemplates,
getById,
getByProjectId,
createRotation,
restartById,
deleteById
};
};
@@ -0,0 +1,24 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateSecretRotationDTO = {
secretPath: string;
environment: string;
interval: number;
provider: string;
inputs: Record<string, unknown>;
outputs: Record<string, string>;
} & TProjectPermission;
export type TListByProjectIdDTO = TProjectPermission;
export type TDeleteDTO = {
rotationId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRestartDTO = {
rotationId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetByIdDTO = {
rotationId: string;
} & Omit<TProjectPermission, "projectId">;
@@ -0,0 +1,28 @@
import { MYSQL_TEMPLATE } from "./mysql";
import { POSTGRES_TEMPLATE } from "./postgres";
import { SENDGRID_TEMPLATE } from "./sendgrid";
import { TSecretRotationProviderTemplate } from "./types";
export const rotationTemplates: TSecretRotationProviderTemplate[] = [
{
name: "sendgrid",
title: "Twilio Sendgrid",
image: "sendgrid.png",
description: "Rotate Twilio Sendgrid API keys",
template: SENDGRID_TEMPLATE
},
{
name: "postgres",
title: "PostgreSQL",
image: "postgres.png",
description: "Rotate PostgreSQL/CockroachDB user credentials",
template: POSTGRES_TEMPLATE
},
{
name: "mysql",
title: "MySQL",
image: "mysql.png",
description: "Rotate MySQL@7/MariaDB user credentials",
template: MYSQL_TEMPLATE
}
];
@@ -0,0 +1,41 @@
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
export const MYSQL_TEMPLATE = {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.MySql,
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const },
port: { type: "integer" as const, default: "3306" },
username1: {
type: "string",
default: "infisical-sql-user1",
desc: "This user must be created in your database"
},
username2: {
type: "string",
default: "infisical-sql-user2",
desc: "This user must be created in your database"
},
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
},
required: [
"admin_username",
"admin_password",
"host",
"database",
"username1",
"username2",
"port"
],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
}
};
@@ -0,0 +1,41 @@
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
export const POSTGRES_TEMPLATE = {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg as const,
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const },
port: { type: "integer" as const, default: "5432" },
username1: {
type: "string",
default: "infisical-pg-user1",
desc: "This user must be created in your database"
},
username2: {
type: "string",
default: "infisical-pg-user2",
desc: "This user must be created in your database"
},
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
},
required: [
"admin_username",
"admin_password",
"host",
"database",
"username1",
"username2",
"port"
],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
}
};
@@ -0,0 +1,62 @@
/* eslint-disable no-template-curly-in-string */
import { TAssignOp, TProviderFunctionTypes } from "./types";
export const SENDGRID_TEMPLATE = {
type: TProviderFunctionTypes.HTTP as const,
inputs: {
type: "object" as const,
properties: {
admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" },
api_key_scopes: {
type: "array",
items: { type: "string" as const },
desc: "Scopes for created tokens by rotation(Array)"
}
},
required: ["admin_api_key", "api_key_scopes"],
additionalProperties: false
},
outputs: {
api_key: { type: "string" }
},
internal: {
api_key_id: { type: "string" }
},
functions: {
set: {
url: "https://api.sendgrid.com/v3/api_keys",
method: "POST",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
body: {
name: "infisical-${random | 16}",
scopes: { ref: "inputs.api_key_scopes" }
},
setter: {
"outputs.api_key": {
assign: TAssignOp.JmesPath as const,
path: "api_key"
},
"internal.api_key_id": {
assign: TAssignOp.JmesPath as const,
path: "api_key_id"
}
}
},
remove: {
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "DELETE"
},
test: {
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "GET"
}
}
};
@@ -0,0 +1,72 @@
export enum TProviderFunctionTypes {
HTTP = "http",
DB = "database"
}
export enum TDbProviderClients {
// postgres, cockroack db, amazon red shift
Pg = "pg",
// mysql and maria db
MySql = "mysql"
}
export enum TAssignOp {
Direct = "direct",
JmesPath = "jmesopath"
}
export type TJmesPathAssignOp = {
assign: TAssignOp.JmesPath;
path: string;
};
export type TDirectAssignOp = {
assign: TAssignOp.Direct;
value: string;
};
export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp;
export type THttpProviderFunction = {
url: string;
method: string;
header?: Record<string, string>;
query?: Record<string, string>;
body?: Record<string, unknown>;
setter?: Record<string, TAssignFunction>;
pre?: Record<string, TDirectAssignOp>;
};
export type TSecretRotationProviderTemplate = {
name: string;
title: string;
image?: string;
description?: string;
template: THttpProviderTemplate | TDbProviderTemplate;
};
export type THttpProviderTemplate = {
type: TProviderFunctionTypes.HTTP;
inputs: {
type: "object";
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
required?: string[];
};
outputs: Record<string, unknown>;
functions: {
set: THttpProviderFunction;
remove?: THttpProviderFunction;
test: THttpProviderFunction;
};
};
export type TDbProviderTemplate = {
type: TProviderFunctionTypes.DB;
client: TDbProviderClients;
inputs: {
type: "object";
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
required?: string[];
};
outputs: Record<string, unknown>;
};