feat(secret-rotation): added db ssl support

This commit is contained in:
Akhil Mohan
2023-11-01 22:21:31 +05:30
parent 97e4338335
commit c9c40521b2
19 changed files with 387 additions and 412 deletions
@@ -140,7 +140,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
query: { secretPath, environment, workspaceId }
} = validatedData;
const {
query: { folderId, include_imports: includeImports }
query: { include_imports: includeImports }
} = validatedData;
// if the service token has single scope, it will get all secrets for that scope by default
@@ -156,13 +156,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId = serviceTokenDetails.workspace.toString();
}
if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) throw BadRequestError({ message: "Folder not found" });
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" });
@@ -177,7 +170,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
secretPath,
authData: req.authData
});
@@ -467,20 +459,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
export const getSecrets = async (req: Request, res: Response) => {
const validatedData = await validateRequest(reqValidator.GetSecretsV3, req);
const {
query: { environment, workspaceId, include_imports: includeImports, folderId }
query: { environment, workspaceId, include_imports: includeImports }
} = validatedData;
let {
query: { secretPath }
} = validatedData;
if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) return res.send({ secrets: [] });
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
@@ -492,7 +477,6 @@ export const getSecrets = async (req: Request, res: Response) => {
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
secretPath,
authData: req.authData
});
@@ -1,215 +0,0 @@
import {
ISecretRotationProviderTemplate,
TProviderFunctionTypes,
TDbProviderClients,
TAssignOp
} from "./types";
const SENDGRID_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_api_key: { type: "string" as const },
scopes: { type: "array", items: { type: "string" as const } }
},
required: ["admin_api_key", "scopes"],
additionalProperties: false
},
outputs: {
api_key: { type: "string" }
},
internal: {
api_key_id: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.HTTP as const,
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.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: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "DELETE"
},
test: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "GET"
}
}
};
const POSTGRES_TEMPLATE = {
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" },
username2: { type: "string", default: "infisical-pg-user2" }
},
required: ["admin_username", "admin_password", "host", "database"],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "SELECT NOW()"
}
}
};
const MYSQL_TEMPLATE = {
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" },
username2: { type: "string", default: "infisical-sql-user2" }
},
required: ["admin_username", "admin_password", "host", "database"],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query:
"ALTER USER ${internal.username} IDENTIFIED WITH mysql_native_password BY '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "SELECT NOW()"
}
}
};
export const providerRotationTemplates: ISecretRotationProviderTemplate[] = [
{
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
}
];
+7 -5
View File
@@ -8,7 +8,7 @@ import mysql from "mysql";
import { client, getRootEncryptionKey } from "../../config";
import { BotService, TelemetryService } from "../../services";
import { SecretRotation } from "./models";
import { providerRotationTemplates } from "./providerTemplates";
import { rotationTemplates } from "./templates";
import {
ISecretRotationData,
ISecretRotationEncData,
@@ -88,13 +88,14 @@ const secretRotationHttpFn = async (
const secretRotationDbFn = async (func: TDbProviderFunction, variables: ISecretRotationData) => {
const { type, client, pre, ...dbConnection } = func;
const { username, password, host, database, port, query } = interpolate(
const { username, password, host, database, port, query, ca } = interpolate(
dbConnection,
getInterpolationValue(variables)
);
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host");
if (client === TDbProviderClients.Pg) {
const pgClient = new PgClient({ user: username, password, host, database, port });
const pgClient = new PgClient({ user: username, password, host, database, port, ssl });
await pgClient.connect();
const res = await pgClient.query(query);
await pgClient.end();
@@ -106,7 +107,8 @@ const secretRotationDbFn = async (func: TDbProviderFunction, variables: ISecretR
host,
database,
port,
connectionLimit: 1
connectionLimit: 1,
ssl
});
const res = await new Promise((resolve, reject) => {
sqlClient.query(query, (err, data) => {
@@ -193,7 +195,7 @@ secretRotationQueue.process(async (job: Job) => {
];
}>("outputs.secret");
const infisicalRotationProvider = providerRotationTemplates.find(
const infisicalRotationProvider = rotationTemplates.find(
({ name }) => name === secretRotation?.provider
);
+4 -12
View File
@@ -1,13 +1,5 @@
import {
ISecretRotationEncData,
ISecretRotationProviderTemplate,
TCreateSecretRotation,
TGetProviderTemplates
} from "./types";
import {
providerRotationTemplates as infisicalRotationTemplates,
providerRotationTemplates
} from "./providerTemplates";
import { ISecretRotationEncData, TCreateSecretRotation, TGetProviderTemplates } from "./types";
import { rotationTemplates } from "./templates";
import { SecretRotation } from "./models";
import { client, getRootEncryptionKey } from "../../config";
import { BadRequestError } from "../../utils/errors";
@@ -19,7 +11,7 @@ const ajv = new Ajv();
export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => {
return {
custom: [],
providers: infisicalRotationTemplates
providers: rotationTemplates
};
};
@@ -32,7 +24,7 @@ export const createSecretRotation = async ({
inputs,
outputs
}: TCreateSecretRotation) => {
const rotationTemplate = providerRotationTemplates.find(({ name }) => name === provider);
const rotationTemplate = rotationTemplates.find(({ name }) => name === provider);
if (!rotationTemplate) throw BadRequestError({ message: "Provider not found" });
const formattedInputs: Record<string, unknown> = {};
@@ -0,0 +1,28 @@
import { ISecretRotationProviderTemplate } from "../types";
import { MYSQL_TEMPLATE } from "./mysql";
import { POSTGRES_TEMPLATE } from "./postgres";
import { SENDGRID_TEMPLATE } from "./sendgrid";
export const rotationTemplates: ISecretRotationProviderTemplate[] = [
{
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,75 @@
import { TProviderFunctionTypes, TDbProviderClients, TAssignOp } from "../types";
export const MYSQL_TEMPLATE = {
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" },
username2: { type: "string", default: "infisical-sql-user2" },
ca: { type: "string" }
},
required: [
"admin_username",
"admin_password",
"host",
"database",
"username1",
"username2",
"port"
],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
ca: "${inputs.ca}",
query:
"ALTER USER ${internal.username} IDENTIFIED WITH mysql_native_password BY '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "SELECT NOW()"
}
}
};
@@ -0,0 +1,74 @@
import { TProviderFunctionTypes, TDbProviderClients, TAssignOp } from "../types";
export const POSTGRES_TEMPLATE = {
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" },
username2: { type: "string", default: "infisical-pg-user2" },
ca: { type: "string" }
},
required: [
"admin_username",
"admin_password",
"host",
"database",
"username1",
"username2",
"port"
],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
ca: "${inputs.ca}",
query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "SELECT NOW()"
}
}
};
@@ -0,0 +1,59 @@
import { TProviderFunctionTypes, TAssignOp } from "../types";
export const SENDGRID_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_api_key: { type: "string" as const },
scopes: { type: "array", items: { type: "string" as const } }
},
required: ["admin_api_key", "scopes"],
additionalProperties: false
},
outputs: {
api_key: { type: "string" }
},
internal: {
api_key_id: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.HTTP as const,
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.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: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "DELETE"
},
test: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "GET"
}
}
};
+11 -3
View File
@@ -553,14 +553,22 @@ export const getSecretsHelper = async ({
workspaceId,
environment,
authData,
folderId,
secretPath = "/"
}: GetSecretsParams) => {
let secrets: ISecret[] = [];
// if using service token filter towards the folderId by secretpath
if (!folderId) {
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
let folderId = "root";
if (!folders && folderId !== "root") return [];
// get folder from folder tree
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) return [];
folderId = folder?.id;
}
// get personal secrets first
@@ -26,7 +26,6 @@ export interface CreateSecretParams {
export interface GetSecretsParams {
workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
secretPath: string;
authData: AuthData;
}
-2
View File
@@ -228,7 +228,6 @@ export const GetSecretsRawV3 = z.object({
workspaceId: z.string().trim().optional(),
environment: z.string().trim().optional(),
secretPath: z.string().trim().default("/"),
folderId: z.string().trim().optional(),
include_imports: z
.enum(["true", "false"])
.default("false")
@@ -302,7 +301,6 @@ export const GetSecretsV3 = z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
folderId: z.string().trim().optional(),
include_imports: z
.enum(["true", "false"])
.default("false")