mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(secret-rotation): added db ssl support
This commit is contained in:
@@ -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
|
||||
}
|
||||
];
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
28
backend/src/ee/secretRotation/templates/index.ts
Normal file
28
backend/src/ee/secretRotation/templates/index.ts
Normal file
@@ -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
|
||||
}
|
||||
];
|
||||
75
backend/src/ee/secretRotation/templates/mysql.ts
Normal file
75
backend/src/ee/secretRotation/templates/mysql.ts
Normal file
@@ -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()"
|
||||
}
|
||||
}
|
||||
};
|
||||
74
backend/src/ee/secretRotation/templates/postgres.ts
Normal file
74
backend/src/ee/secretRotation/templates/postgres.ts
Normal file
@@ -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()"
|
||||
}
|
||||
}
|
||||
};
|
||||
59
backend/src/ee/secretRotation/templates/sendgrid.ts
Normal file
59
backend/src/ee/secretRotation/templates/sendgrid.ts
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
1
frontend/public/lotties/rotation.json
Normal file
1
frontend/public/lotties/rotation.json
Normal file
File diff suppressed because one or more lines are too long
@@ -32,7 +32,7 @@ export const Stepper = ({ activeStep, children, direction, className }: StepperP
|
||||
<div className="flex items-center space-x-2 flex-shrink-0">
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-6 h-6 flex items-center justify-center font-medium text-mineshaft-800 text-sm rounded-full transition-all",
|
||||
"w-7 h-7 flex items-center justify-center font-medium text-mineshaft-800 text-sm rounded-full transition-all",
|
||||
isCompleted ? "bg-primary" : "border text-bunker-300 border-primary/30",
|
||||
isActive && "bg-primary text-mineshaft-800"
|
||||
)}
|
||||
@@ -70,7 +70,7 @@ export type StepProps = {
|
||||
|
||||
export const Step = ({ title, description }: StepProps) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-1 text-gray-300">
|
||||
<div className="flex flex-col text-gray-300">
|
||||
<div className="font-medium text-sm">{title}</div>
|
||||
{description && <div className="text-xs">{description}</div>}
|
||||
</div>
|
||||
|
||||
@@ -503,7 +503,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?._id}/secret-rotation`
|
||||
}
|
||||
icon="system-outline-189-domain-verification"
|
||||
icon="rotation"
|
||||
>
|
||||
Secret rotation
|
||||
</MenuItem>
|
||||
|
||||
@@ -163,10 +163,10 @@ export const SecretRotationPage = withProjectPermission(
|
||||
};
|
||||
|
||||
const handleCreateRotation = async (provider: TSecretRotationProvider) => {
|
||||
if (subscription && !subscription?.secretRotation) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
// if (subscription && !subscription?.secretRotation) {
|
||||
// handlePopUpOpen("upgradePlan");
|
||||
// return;
|
||||
// }
|
||||
if (!canCreateRotation) {
|
||||
createNotification({ type: "error", text: "Access permission denied!!" });
|
||||
return;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TSecretRotationProvider } from "@app/hooks/api/types";
|
||||
|
||||
import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider";
|
||||
|
||||
import { GeneralDetailsForm, TFormSchema as TGeneralFormSchema } from "./steps/GeneralDetailsForm";
|
||||
import { RotationInputForm } from "./steps/RotationInputForm";
|
||||
import {
|
||||
RotationOutputForm,
|
||||
@@ -16,13 +15,12 @@ import {
|
||||
|
||||
const WIZARD_STEPS = [
|
||||
{
|
||||
title: "General"
|
||||
title: "Inputs",
|
||||
description: "Provider secrets"
|
||||
},
|
||||
{
|
||||
title: "Inputs"
|
||||
},
|
||||
{
|
||||
title: "Secret Mapping"
|
||||
title: "Outputs",
|
||||
description: "Map rotated secrets to keys"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -43,7 +41,6 @@ export const CreateRotationForm = ({
|
||||
}: Props) => {
|
||||
const [wizardStep, setWizardStep] = useState(0);
|
||||
const wizardData = useRef<{
|
||||
general?: TGeneralFormSchema;
|
||||
input?: Record<string, string>;
|
||||
output?: TRotationOutputSchema;
|
||||
}>({});
|
||||
@@ -58,18 +55,17 @@ export const CreateRotationForm = ({
|
||||
};
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
if (!wizardData.current.general || !wizardData.current.input || !wizardData.current.output)
|
||||
return;
|
||||
if (!wizardData.current.input || !wizardData.current.output) return;
|
||||
try {
|
||||
await createSecretRotation({
|
||||
workspaceId,
|
||||
provider: provider.name,
|
||||
customProvider,
|
||||
secretPath: wizardData.current.general.secretPath,
|
||||
environment: wizardData.current.general.environment,
|
||||
interval: wizardData.current.general.interval,
|
||||
secretPath: wizardData.current.output.secretPath,
|
||||
environment: wizardData.current.output.environment,
|
||||
interval: wizardData.current.output.interval,
|
||||
inputs: wizardData.current.input,
|
||||
outputs: wizardData.current.output
|
||||
outputs: wizardData.current.output.secrets
|
||||
});
|
||||
setWizardStep(0);
|
||||
onToggle(false);
|
||||
@@ -98,49 +94,50 @@ export const CreateRotationForm = ({
|
||||
className="max-w-2xl"
|
||||
>
|
||||
<Stepper activeStep={wizardStep} direction="horizontal" className="mb-4">
|
||||
{WIZARD_STEPS.map(({ title }, index) => (
|
||||
<Step title={title} key={`wizard-stepper-rotation-${index + 1}`} />
|
||||
{WIZARD_STEPS.map(({ title, description }, index) => (
|
||||
<Step
|
||||
title={title}
|
||||
description={description}
|
||||
key={`wizard-stepper-rotation-${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</Stepper>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{wizardStep === 0 && (
|
||||
<motion.div
|
||||
key="general-step"
|
||||
key="input-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<GeneralDetailsForm
|
||||
<RotationInputForm
|
||||
onCancel={handleFormCancel}
|
||||
onSubmit={(data) => {
|
||||
wizardData.current.general = data;
|
||||
wizardData.current.input = data;
|
||||
setWizardStep((state) => state + 1);
|
||||
}}
|
||||
inputSchema={provider.template?.inputs || {}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === 1 && (
|
||||
<RotationInputForm
|
||||
onCancel={handleFormCancel}
|
||||
onSubmit={(data) => {
|
||||
wizardData.current.input = data;
|
||||
setWizardStep((state) => state + 1);
|
||||
}}
|
||||
inputSchema={provider.template?.inputs || {}}
|
||||
/>
|
||||
)}
|
||||
{wizardStep === 2 && (
|
||||
<RotationOutputForm
|
||||
environment={wizardData.current.general?.environment || ""}
|
||||
secretPath={wizardData.current.general?.secretPath || "/"}
|
||||
outputSchema={provider.template?.outputs || {}}
|
||||
onCancel={handleFormCancel}
|
||||
onSubmit={async (data) => {
|
||||
wizardData.current.output = data;
|
||||
await handleFormSubmit();
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
key="output-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<RotationOutputForm
|
||||
outputSchema={provider.template?.outputs || {}}
|
||||
onCancel={handleFormCancel}
|
||||
onSubmit={async (data) => {
|
||||
wizardData.current.output = data;
|
||||
await handleFormSubmit();
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
|
||||
const formSchema = z.object({
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
interval: z.number()
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
type Props = {
|
||||
onSubmit: (data: TFormSchema) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export const GeneralDetailsForm = ({ onSubmit, onCancel }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
defaultValue="/"
|
||||
render={({ field }) => (
|
||||
<FormControl className="capitalize" label="Secret path">
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="interval"
|
||||
defaultValue={15}
|
||||
render={({ field }) => (
|
||||
<FormControl className="capitalize" label="Rotation Interval (Days)">
|
||||
<Input
|
||||
{...field}
|
||||
min={1}
|
||||
type="number"
|
||||
onChange={(evt) => field.onChange(parseInt(evt.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
Next
|
||||
</Button>
|
||||
<Button onClick={onCancel} colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -34,7 +34,6 @@ export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) =>
|
||||
defaultValue={inputSchema.properties[inputName]?.default}
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
className="capitalize"
|
||||
key={`provider-input-${inputName}`}
|
||||
label={inputName.replaceAll("_", " ")}
|
||||
helperText={inputSchema.properties[inputName]?.helperText}
|
||||
|
||||
@@ -2,40 +2,42 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.record(z.string());
|
||||
const formSchema = z.object({
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
interval: z.number().min(1),
|
||||
secrets: z.record(z.string())
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
type Props = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
outputSchema: Record<string, unknown>;
|
||||
onSubmit: (data: TFormSchema) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export const RotationOutputForm = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
outputSchema = {}
|
||||
}: Props) => {
|
||||
export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const environment = watch("environment", environments?.[0]?.slug);
|
||||
const secretPath = watch("secretPath");
|
||||
|
||||
const { data: userWsKey } = useGetUserWsKey(workspaceId);
|
||||
const { data: secrets } = useGetProjectSecrets({
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -44,11 +46,65 @@ export const RotationOutputForm = ({
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
defaultValue="/"
|
||||
render={({ field }) => (
|
||||
<FormControl className="capitalize" label="Secret path">
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="interval"
|
||||
defaultValue={15}
|
||||
render={({ field }) => (
|
||||
<FormControl className="capitalize" label="Rotation Interval (Days)">
|
||||
<Input
|
||||
{...field}
|
||||
min={1}
|
||||
type="number"
|
||||
onChange={(evt) => field.onChange(parseInt(evt.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col mt-4 pt-4 mb-2 border-t border-bunker-300/30">
|
||||
<div>Mapping</div>
|
||||
<div className="text-bunker-300 text-sm">Select keys for rotated value to get saved</div>
|
||||
</div>
|
||||
{Object.keys(outputSchema).map((outputName) => (
|
||||
<Controller
|
||||
key={`provider-output-${outputName}`}
|
||||
control={control}
|
||||
name={outputName}
|
||||
name={`secrets.${outputName}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl className="uppercase" label={outputName.replaceAll("_", " ")} isRequired>
|
||||
<Select
|
||||
@@ -57,11 +113,22 @@ export const RotationOutputForm = ({
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
>
|
||||
{secrets?.map(({ key, _id }) => (
|
||||
<SelectItem value={_id} key={_id}>
|
||||
{key}
|
||||
{!isSecretsLoading &&
|
||||
secrets?.map(({ key, _id }) => (
|
||||
<SelectItem value={_id} key={_id}>
|
||||
{key}
|
||||
</SelectItem>
|
||||
))}
|
||||
{isSecretsLoading && (
|
||||
<SelectItem value="Loading" isDisabled>
|
||||
<Spinner size="xs" />
|
||||
</SelectItem>
|
||||
))}
|
||||
)}
|
||||
{!isSecretsLoading && secrets?.length === 0 && (
|
||||
<SelectItem value="Empty" isDisabled>
|
||||
No secrets found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user