mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
41 Commits
blueprint-
...
misc/final
Author | SHA1 | Date | |
---|---|---|---|
ada63b9e7d | |||
f91f9c9487 | |||
f0d19e4701 | |||
7eeff6c406 | |||
132c3080bb | |||
bf09fa33fa | |||
a87e7b792c | |||
e8ca020903 | |||
a603938488 | |||
cff7981fe0 | |||
b39d5c6682 | |||
dd1f1d07cc | |||
c3f8c55672 | |||
75aeef3897 | |||
c97fe77aec | |||
3e16d7e160 | |||
6bf4b4a380 | |||
9dedaa6779 | |||
8eab7d2f01 | |||
4e796e7e41 | |||
c6fa647825 | |||
496cebb08f | |||
33db6df7f2 | |||
88d25e97e9 | |||
4ad9fa1ad1 | |||
1642fb42d8 | |||
3983c2bc4a | |||
34d87ca30f | |||
12b6f27151 | |||
ea426e8b2d | |||
4d567f0b08 | |||
6548372e3b | |||
77af640c4c | |||
90f85152bc | |||
cfa8770bdc | |||
be8562824d | |||
4f1fe8a9fa | |||
b0031b71e0 | |||
7503876ca0 | |||
dfe36f346f | |||
f9ca9b51b2 |
@ -78,3 +78,5 @@ PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=
|
||||
|
@ -118,9 +118,9 @@ describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
|
||||
value: "stage-value"
|
||||
});
|
||||
|
||||
// wait for 5 second for replication to finish
|
||||
// wait for 10 second for replication to finish
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 5000); // time to breathe for db
|
||||
setTimeout(resolve, 10000); // time to breathe for db
|
||||
});
|
||||
|
||||
const secret = await getSecretByNameV2({
|
||||
@ -173,9 +173,9 @@ describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
|
||||
value: "prod-value"
|
||||
});
|
||||
|
||||
// wait for 5 second for replication to finish
|
||||
// wait for 10 second for replication to finish
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 5000); // time to breathe for db
|
||||
setTimeout(resolve, 10000); // time to breathe for db
|
||||
});
|
||||
|
||||
const secret = await getSecretByNameV2({
|
||||
@ -343,9 +343,9 @@ describe.each([{ path: "/" }, { path: "/deep" }])(
|
||||
value: "prod-value"
|
||||
});
|
||||
|
||||
// wait for 5 second for replication to finish
|
||||
// wait for 10 second for replication to finish
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 5000); // time to breathe for db
|
||||
setTimeout(resolve, 10000); // time to breathe for db
|
||||
});
|
||||
|
||||
const secret = await getSecretByNameV2({
|
||||
|
@ -8,61 +8,80 @@ const prompt = promptSync({
|
||||
sigint: true
|
||||
});
|
||||
|
||||
const sanitizeInputParam = (value: string) => {
|
||||
// Escape double quotes and wrap the entire value in double quotes
|
||||
if (value) {
|
||||
return `"${value.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return '""';
|
||||
};
|
||||
|
||||
const exportDb = () => {
|
||||
const exportHost = prompt("Enter your Postgres Host to migrate from: ");
|
||||
const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432";
|
||||
const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical";
|
||||
const exportPassword = prompt("Enter your Postgres Password to migrate from: ");
|
||||
const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical";
|
||||
const exportHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate from: "));
|
||||
const exportPort = sanitizeInputParam(
|
||||
prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432"
|
||||
);
|
||||
const exportUser = sanitizeInputParam(
|
||||
prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
const exportPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate from: "));
|
||||
const exportDatabase = sanitizeInputParam(
|
||||
prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
|
||||
// we do not include the audit_log and secret_sharing entries
|
||||
execSync(
|
||||
`PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
|
||||
`PGDATABASE=${exportDatabase} PGPASSWORD=${exportPassword} PGHOST=${exportHost} PGPORT=${exportPort} PGUSER=${exportUser} pg_dump -Fc infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
|
||||
__dirname,
|
||||
"../src/db/dump.sql"
|
||||
"../src/db/backup.dump"
|
||||
)}`,
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
};
|
||||
|
||||
const importDbForOrg = () => {
|
||||
const importHost = prompt("Enter your Postgres Host to migrate to: ");
|
||||
const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432";
|
||||
const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical";
|
||||
const importPassword = prompt("Enter your Postgres Password to migrate to: ");
|
||||
const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical";
|
||||
const orgId = prompt("Enter the organization ID to migrate: ");
|
||||
const importHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate to: "));
|
||||
const importPort = sanitizeInputParam(prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432");
|
||||
const importUser = sanitizeInputParam(
|
||||
prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
const importPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate to: "));
|
||||
const importDatabase = sanitizeInputParam(
|
||||
prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical"
|
||||
);
|
||||
const orgId = sanitizeInputParam(prompt("Enter the organization ID to migrate: "));
|
||||
|
||||
if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) {
|
||||
if (!existsSync(path.join(__dirname, "../src/db/backup.dump"))) {
|
||||
console.log("File not found, please export the database first.");
|
||||
return;
|
||||
}
|
||||
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join(
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} pg_restore -d ${importDatabase} --verbose ${path.join(
|
||||
__dirname,
|
||||
"../src/db/dump.sql"
|
||||
)}`
|
||||
"../src/db/backup.dump"
|
||||
)}`,
|
||||
{ maxBuffer: 1024 * 1024 * 4096 }
|
||||
);
|
||||
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
|
||||
);
|
||||
|
||||
// delete global/instance-level resources not relevant to the organization to migrate
|
||||
// users
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
|
||||
);
|
||||
|
||||
// identities
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
|
||||
);
|
||||
|
||||
// reset slack configuration in superAdmin
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
|
||||
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
|
||||
);
|
||||
|
||||
console.log("Organization migrated successfully.");
|
||||
|
@ -72,5 +72,5 @@ export async function down(knex: Knex): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const config = {transaction: false};
|
||||
const config = { transaction: false };
|
||||
export { config };
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||
t.dropForeign("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||
t.dropForeign("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization);
|
||||
});
|
||||
}
|
||||
}
|
@ -267,7 +267,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
: "",
|
||||
secretComment: el.secretVersion.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: ""
|
||||
: "",
|
||||
tags: el.secretVersion.tags
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
@ -571,7 +572,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
reminderNote: el.reminderNote,
|
||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||
key: el.key,
|
||||
tagIds: el?.tags.map(({ id }) => id),
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
...encryptedValue
|
||||
}
|
||||
};
|
||||
|
@ -85,7 +85,8 @@ export const secretRotationDbFn = async ({
|
||||
password,
|
||||
username,
|
||||
client,
|
||||
variables
|
||||
variables,
|
||||
options
|
||||
}: TSecretRotationDbFn) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@ -117,7 +118,8 @@ export const secretRotationDbFn = async ({
|
||||
password,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 }
|
||||
pool: { min: 0, max: 1 },
|
||||
options
|
||||
}
|
||||
});
|
||||
const data = await db.raw(query, variables);
|
||||
@ -153,6 +155,14 @@ export const getDbSetQuery = (db: TDbProviderClients, variables: { username: str
|
||||
variables: [variables.username]
|
||||
};
|
||||
}
|
||||
|
||||
if (db === TDbProviderClients.MsSqlServer) {
|
||||
return {
|
||||
query: `ALTER LOGIN ?? WITH PASSWORD = '${variables.password}'`,
|
||||
variables: [variables.username]
|
||||
};
|
||||
}
|
||||
|
||||
// add more based on client
|
||||
return {
|
||||
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,
|
||||
|
@ -24,4 +24,5 @@ export type TSecretRotationDbFn = {
|
||||
query: string;
|
||||
variables: unknown[];
|
||||
ca?: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
@ -94,7 +94,9 @@ export const secretRotationQueueFactory = ({
|
||||
// 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
|
||||
}
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -114,6 +116,7 @@ export const secretRotationQueueFactory = ({
|
||||
|
||||
queue.start(QueueName.SecretRotation, async (job) => {
|
||||
const { rotationId } = job.data;
|
||||
const appCfg = getConfig();
|
||||
logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`);
|
||||
const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
const rotationProvider = rotationTemplates.find(({ name }) => name === secretRotation?.provider);
|
||||
@ -172,6 +175,15 @@ export const secretRotationQueueFactory = ({
|
||||
// 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 options =
|
||||
provider.template.client === TDbProviderClients.MsSqlServer
|
||||
? ({
|
||||
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
|
||||
cryptoCredentialsDetails: ca ? { ca } : {}
|
||||
} as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
const dbFunctionArg = {
|
||||
username,
|
||||
password,
|
||||
@ -179,8 +191,10 @@ export const secretRotationQueueFactory = ({
|
||||
database,
|
||||
port,
|
||||
ca: ca as string,
|
||||
client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client
|
||||
client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client,
|
||||
options
|
||||
} as TSecretRotationDbFn;
|
||||
|
||||
// set function
|
||||
await secretRotationDbFn({
|
||||
...dbFunctionArg,
|
||||
@ -189,12 +203,17 @@ export const secretRotationQueueFactory = ({
|
||||
username: newCredential.internal.username as string
|
||||
})
|
||||
});
|
||||
|
||||
// test function
|
||||
const testQuery =
|
||||
provider.template.client === TDbProviderClients.MsSqlServer ? "SELECT GETDATE()" : "SELECT NOW()";
|
||||
|
||||
await secretRotationDbFn({
|
||||
...dbFunctionArg,
|
||||
query: "SELECT NOW()",
|
||||
query: testQuery,
|
||||
variables: []
|
||||
});
|
||||
|
||||
newCredential.outputs.db_username = newCredential.internal.username;
|
||||
newCredential.outputs.db_password = newCredential.internal.rotated_password;
|
||||
// clean up
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AWS_IAM_TEMPLATE } from "./aws-iam";
|
||||
import { MSSQL_TEMPLATE } from "./mssql";
|
||||
import { MYSQL_TEMPLATE } from "./mysql";
|
||||
import { POSTGRES_TEMPLATE } from "./postgres";
|
||||
import { SENDGRID_TEMPLATE } from "./sendgrid";
|
||||
@ -26,6 +27,13 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
||||
description: "Rotate MySQL@7/MariaDB user credentials",
|
||||
template: MYSQL_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "mssql",
|
||||
title: "Microsoft SQL Server",
|
||||
image: "mssqlserver.png",
|
||||
description: "Rotate Microsoft SQL server user credentials",
|
||||
template: MSSQL_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "aws-iam",
|
||||
title: "AWS IAM",
|
||||
|
33
backend/src/ee/services/secret-rotation/templates/mssql.ts
Normal file
33
backend/src/ee/services/secret-rotation/templates/mssql.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
|
||||
|
||||
export const MSSQL_TEMPLATE = {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.MsSqlServer,
|
||||
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, default: "master" },
|
||||
port: { type: "integer" as const, default: "1433" },
|
||||
username1: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user1",
|
||||
desc: "SQL Server login name that must be created at server level with a matching database user"
|
||||
},
|
||||
username2: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user2",
|
||||
desc: "SQL Server login name that must be created at server level with a matching database user"
|
||||
},
|
||||
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" }
|
||||
}
|
||||
};
|
@ -8,7 +8,9 @@ export enum TDbProviderClients {
|
||||
// postgres, cockroack db, amazon red shift
|
||||
Pg = "pg",
|
||||
// mysql and maria db
|
||||
MySql = "mysql"
|
||||
MySql = "mysql",
|
||||
|
||||
MsSqlServer = "mssql"
|
||||
}
|
||||
|
||||
export enum TAwsProviderSystems {
|
||||
|
@ -29,7 +29,7 @@ export const KeyStorePrefixes = {
|
||||
};
|
||||
|
||||
export const KeyStoreTtls = {
|
||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10,
|
||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
|
||||
AccessTokenStatusUpdateInSeconds: 120
|
||||
};
|
||||
|
||||
|
@ -162,7 +162,8 @@ const envSchema = z
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true")
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
@ -126,6 +126,8 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
return findRootInheritedSecret(inheritedEnv.variables[secretName], secretName, envs);
|
||||
};
|
||||
|
||||
const targetIdToFolderIdsMap = new Map<string, string>();
|
||||
|
||||
const processBranches = () => {
|
||||
for (const subEnv of parsedJson.subEnvironments) {
|
||||
const app = parsedJson.apps.find((a) => a.id === subEnv.envParentId);
|
||||
@ -135,12 +137,21 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
// Handle regular app branches
|
||||
const branchEnvironment = infisicalImportData.environments.find((e) => e.id === subEnv.parentEnvironmentId);
|
||||
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: subEnv.parentEnvironmentId,
|
||||
environmentId: branchEnvironment!.id,
|
||||
id: subEnv.id
|
||||
});
|
||||
// check if the folder already exists in the same parent environment with the same name
|
||||
|
||||
const folderExists = infisicalImportData.folders.some(
|
||||
(f) => f.name === subEnv.subName && f.parentFolderId === subEnv.parentEnvironmentId
|
||||
);
|
||||
|
||||
// No need to map to target ID's here, because we are not dealing with blocks
|
||||
if (!folderExists) {
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: subEnv.parentEnvironmentId,
|
||||
environmentId: branchEnvironment!.id,
|
||||
id: subEnv.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (block) {
|
||||
@ -162,13 +173,22 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchingAppEnv) continue;
|
||||
|
||||
// 3. Create a folder in the matching app environment
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: matchingAppEnv.id,
|
||||
environmentId: matchingAppEnv.id,
|
||||
id: `${subEnv.id}-${appId}` // Create unique ID for each app's copy of the branch
|
||||
});
|
||||
const folderExists = infisicalImportData.folders.some(
|
||||
(f) => f.name === subEnv.subName && f.parentFolderId === matchingAppEnv.id
|
||||
);
|
||||
|
||||
if (!folderExists) {
|
||||
// 3. Create a folder in the matching app environment
|
||||
infisicalImportData.folders.push({
|
||||
name: subEnv.subName,
|
||||
parentFolderId: matchingAppEnv.id,
|
||||
environmentId: matchingAppEnv.id,
|
||||
id: `${subEnv.id}-${appId}` // Create unique ID for each app's copy of the branch
|
||||
});
|
||||
} else {
|
||||
// folder already exists, so lets map the old folder id to the new folder id
|
||||
targetIdToFolderIdsMap.set(subEnv.id, `${subEnv.id}-${appId}`);
|
||||
}
|
||||
|
||||
// 4. Process secrets in the block branch for this app
|
||||
const branchSecrets = parsedJson.envs[subEnv.id]?.variables || {};
|
||||
@ -408,17 +428,18 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
|
||||
// Process each secret in this environment or branch
|
||||
for (const [secretName, secretData] of Object.entries(envData.variables)) {
|
||||
const environmentId = subEnv ? subEnv.parentEnvironmentId : env;
|
||||
const indexOfExistingSecret = infisicalImportData.secrets.findIndex(
|
||||
(s) => s.name === secretName && s.environmentId === environmentId
|
||||
(s) =>
|
||||
s.name === secretName &&
|
||||
(s.environmentId === subEnv?.parentEnvironmentId || s.environmentId === env) &&
|
||||
(s.folderId ? s.folderId === subEnv?.id : true) &&
|
||||
(secretData.val ? s.value === secretData.val : true)
|
||||
);
|
||||
|
||||
if (secretData.inheritsEnvironmentId) {
|
||||
const resolvedSecret = findRootInheritedSecret(secretData, secretName, parsedJson.envs);
|
||||
|
||||
// Check if there's already a secret with this name in the environment, if there is, we should override it. Because if there's already one, we know its coming from a block.
|
||||
// Variables from the normal environment should take precedence over variables from the block.
|
||||
|
||||
if (indexOfExistingSecret !== -1) {
|
||||
// if a existing secret is found, we should replace it directly
|
||||
const newSecret: (typeof infisicalImportData.secrets)[number] = {
|
||||
@ -456,12 +477,14 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
continue;
|
||||
}
|
||||
|
||||
const folderId = targetIdToFolderIdsMap.get(subEnv?.id || "") || subEnv?.id;
|
||||
|
||||
infisicalImportData.secrets.push({
|
||||
id: randomUUID(),
|
||||
name: secretName,
|
||||
environmentId: subEnv ? subEnv.parentEnvironmentId : env,
|
||||
value: secretData.val || "",
|
||||
...(subEnv && { folderId: subEnv.id }) // Add folderId if this is a branch secret
|
||||
...(folderId && { folderId })
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -591,6 +614,7 @@ export const importDataIntoInfisicalFn = async ({
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
folderId?: string;
|
||||
isFromBlock?: boolean;
|
||||
}[]
|
||||
>();
|
||||
|
||||
@ -599,6 +623,8 @@ export const importDataIntoInfisicalFn = async ({
|
||||
|
||||
// Skip if we can't find either an environment or folder mapping for this secret
|
||||
if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
|
||||
logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret");
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
@ -606,10 +632,22 @@ export const importDataIntoInfisicalFn = async ({
|
||||
if (!mappedToEnvironmentId.has(targetId)) {
|
||||
mappedToEnvironmentId.set(targetId, []);
|
||||
}
|
||||
|
||||
const alreadyHasSecret = mappedToEnvironmentId
|
||||
.get(targetId)!
|
||||
.find((el) => el.secretKey === secret.name && el.folderId === secret.folderId);
|
||||
|
||||
if (alreadyHasSecret && alreadyHasSecret.isFromBlock) {
|
||||
// remove the existing secret if any
|
||||
mappedToEnvironmentId
|
||||
.get(targetId)!
|
||||
.splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1);
|
||||
}
|
||||
mappedToEnvironmentId.get(targetId)!.push({
|
||||
secretKey: secret.name,
|
||||
secretValue: secret.value || "",
|
||||
folderId: secret.folderId
|
||||
folderId: secret.folderId,
|
||||
isFromBlock: secret.appBlockOrderIndex !== undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2540,7 +2540,7 @@ const syncSecretsAzureDevops = async ({
|
||||
|
||||
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
|
||||
|
||||
const variables: Record<string, { value: string, isSecret: boolean }> = {};
|
||||
const variables: Record<string, { value: string; isSecret: boolean }> = {};
|
||||
for (const key of Object.keys(secrets)) {
|
||||
variables[key] = { value: secrets[key].value, isSecret: true };
|
||||
}
|
||||
|
@ -112,6 +112,8 @@ export type TGetSecrets = {
|
||||
};
|
||||
|
||||
const MAX_SYNC_SECRET_DEPTH = 5;
|
||||
const SYNC_SECRET_DEBOUNCE_INTERVAL_MS = 3000;
|
||||
|
||||
export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
|
||||
`secret-queue-dedupe-${environment}-${secretPath}`;
|
||||
|
||||
@ -169,6 +171,39 @@ export const secretQueueFactory = ({
|
||||
);
|
||||
};
|
||||
|
||||
const $generateActor = async (actorId?: string, isManual?: boolean): Promise<Actor> => {
|
||||
if (isManual && actorId) {
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
userId: user.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
};
|
||||
};
|
||||
|
||||
const $getJobKey = (projectId: string, environmentSlug: string, secretPath: string) => {
|
||||
// the idea here is a timestamp based id which will be constant in a 3s interval
|
||||
const timestampId = Math.floor(Date.now() / SYNC_SECRET_DEBOUNCE_INTERVAL_MS);
|
||||
|
||||
return `secret-queue-sync_${projectId}_${environmentSlug}_${secretPath}_${timestampId}`
|
||||
.replace("/", "-")
|
||||
.replace(":", "-");
|
||||
};
|
||||
|
||||
const addSecretReminder = async ({ oldSecret, newSecret, projectId }: TCreateSecretReminderDTO) => {
|
||||
try {
|
||||
const appCfg = getConfig();
|
||||
@ -466,7 +501,7 @@ export const secretQueueFactory = ({
|
||||
dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> }
|
||||
) => {
|
||||
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
|
||||
attempts: 3,
|
||||
attempts: 5,
|
||||
delay: 1000,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
@ -479,10 +514,10 @@ export const secretQueueFactory = ({
|
||||
|
||||
const replicateSecrets = async (dto: Omit<TSyncSecretsDTO, "deDupeQueue">) => {
|
||||
await queueService.queue(QueueName.SecretReplication, QueueJobs.SecretReplication, dto, {
|
||||
attempts: 3,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 2000
|
||||
delay: 3000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
@ -499,6 +534,7 @@ export const secretQueueFactory = ({
|
||||
logger.info(
|
||||
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environmentSlug}] [path=${dto.secretPath}]`
|
||||
);
|
||||
|
||||
const deDuplicationKey = uniqueSecretQueueKey(dto.environmentSlug, dto.secretPath);
|
||||
if (
|
||||
!dto.excludeReplication
|
||||
@ -523,7 +559,8 @@ export const secretQueueFactory = ({
|
||||
{
|
||||
removeOnFail: true,
|
||||
removeOnComplete: true,
|
||||
delay: 1000,
|
||||
jobId: $getJobKey(dto.projectId, dto.environmentSlug, dto.secretPath),
|
||||
delay: SYNC_SECRET_DEBOUNCE_INTERVAL_MS,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
@ -532,7 +569,6 @@ export const secretQueueFactory = ({
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.isSmtpConfigured) return;
|
||||
@ -540,7 +576,6 @@ export const secretQueueFactory = ({
|
||||
await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, {
|
||||
jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`,
|
||||
delay: 1_000 * 60, // 1 minute
|
||||
|
||||
removeOnFail: true,
|
||||
removeOnComplete: true
|
||||
});
|
||||
@ -733,80 +768,51 @@ export const secretQueueFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||
const toBeSyncedIntegrations = integrations.filter(
|
||||
// note: sync only the integrations sourced from secretPath
|
||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||
const lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
|
||||
60000,
|
||||
{
|
||||
retryCount: 10,
|
||||
retryDelay: 3000,
|
||||
retryJitter: 500
|
||||
}
|
||||
);
|
||||
|
||||
const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = [];
|
||||
|
||||
if (!integrations.length) return;
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
|
||||
const lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
|
||||
10000,
|
||||
{
|
||||
retryCount: 3,
|
||||
retryDelay: 2000
|
||||
}
|
||||
);
|
||||
const lockAcquiredTime = new Date();
|
||||
|
||||
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
|
||||
);
|
||||
|
||||
// check whether the integration should wait or not
|
||||
if (lastRunSyncIntegrationTimestamp) {
|
||||
const INTEGRATION_INTERVAL = 2000;
|
||||
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
|
||||
if (isStaleSyncIntegration) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
|
||||
lockAcquiredTime.toISOString(),
|
||||
lastRunSyncIntegrationTimestamp
|
||||
);
|
||||
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const generateActor = async (): Promise<Actor> => {
|
||||
if (isManual && actorId) {
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
userId: user.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
};
|
||||
};
|
||||
|
||||
// akhilmhdh: this try catch is for lock release
|
||||
try {
|
||||
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
|
||||
);
|
||||
|
||||
// check whether the integration should wait or not
|
||||
if (lastRunSyncIntegrationTimestamp) {
|
||||
const INTEGRATION_INTERVAL = 2000;
|
||||
|
||||
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
|
||||
lockAcquiredTime.toISOString(),
|
||||
lastRunSyncIntegrationTimestamp
|
||||
);
|
||||
|
||||
// give some time for integration to breath
|
||||
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL)
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, INTEGRATION_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||
const toBeSyncedIntegrations = integrations.filter(
|
||||
// note: sync only the integrations sourced from secretPath
|
||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||
);
|
||||
|
||||
if (!integrations.length) return;
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
const secrets = shouldUseSecretV2Bridge
|
||||
? await getIntegrationSecretsV2({
|
||||
environment,
|
||||
@ -892,7 +898,7 @@ export const secretQueueFactory = ({
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId,
|
||||
actor: await generateActor(),
|
||||
actor: await $generateActor(actorId, isManual),
|
||||
event: {
|
||||
type: EventType.INTEGRATION_SYNCED,
|
||||
metadata: {
|
||||
@ -931,7 +937,7 @@ export const secretQueueFactory = ({
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId,
|
||||
actor: await generateActor(),
|
||||
actor: await $generateActor(actorId, isManual),
|
||||
event: {
|
||||
type: EventType.INTEGRATION_SYNCED,
|
||||
metadata: {
|
||||
|
@ -4,6 +4,17 @@ title: "Changelog"
|
||||
|
||||
The changelog below reflects new product developments and updates on a monthly basis.
|
||||
|
||||
|
||||
## October 2024
|
||||
- Significantly improved performance of audit log operations in UI.
|
||||
- Released [Databricks integration](https://infisical.com/docs/integrations/cloud/databricks).
|
||||
- Added ability to enforce 2FA organization-wide.
|
||||
- Added multiple resource to the [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs), including AWS and GCP integrations.
|
||||
- Released [Infisical KMS](https://infisical.com/docs/documentation/platform/kms/overview).
|
||||
- Added support for [LDAP dynamic secrets](https://infisical.com/docs/documentation/platform/ldap/overview).
|
||||
- Enabled changing auth methods for machine identities in the UI.
|
||||
- Launched [Infisical EU Cloud](https://eu.infisical.com).
|
||||
|
||||
## September 2024
|
||||
- Improved paginations for identities and secrets.
|
||||
- Significant improvements to the [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
|
||||
|
@ -4,7 +4,7 @@ sidebarTitle: "Overview"
|
||||
description: "Learn more about identities to interact with resources in Infisical."
|
||||
---
|
||||
|
||||
To interact with secrets and resource with Infisical, it is important to undrestand the concept of identities.
|
||||
To interact with secrets and resource with Infisical, it is important to understand the concept of identities.
|
||||
Identities can be of two types:
|
||||
- **People** (e.g., developers, platform engineers, administrators)
|
||||
- **Machines** (e.g., machine entities for managing secrets in CI/CD pipelines, production applications, and more)
|
||||
|
139
docs/documentation/platform/secret-rotation/mssql.mdx
Normal file
139
docs/documentation/platform/secret-rotation/mssql.mdx
Normal file
@ -0,0 +1,139 @@
|
||||
---
|
||||
title: "Microsoft SQL Server"
|
||||
description: "Learn how to automatically rotate Microsoft SQL Server user passwords."
|
||||
---
|
||||
|
||||
The Infisical SQL Server secret rotation allows you to automatically rotate your database users' passwords at a predefined interval.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Create two SQL Server logins and database users with the required permissions. We'll refer to them as `user-a` and `user-b`.
|
||||
2. Create another SQL Server login with permissions to alter logins for `user-a` and `user-b`. We'll refer to this as the `admin` login.
|
||||
|
||||
Here's how to set up the prerequisites:
|
||||
|
||||
```sql
|
||||
-- Create the logins (at server level)
|
||||
CREATE LOGIN [user-a] WITH PASSWORD = 'ComplexPassword1';
|
||||
CREATE LOGIN [user-b] WITH PASSWORD = 'ComplexPassword2';
|
||||
|
||||
-- Create database users for the logins (in your specific database)
|
||||
USE [YourDatabase];
|
||||
CREATE USER [user-a] FOR LOGIN [user-a];
|
||||
CREATE USER [user-b] FOR LOGIN [user-b];
|
||||
|
||||
-- Grant necessary permissions to the users
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-a];
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-b];
|
||||
|
||||
-- Create admin login with permission to alter other logins
|
||||
CREATE LOGIN [admin] WITH PASSWORD = 'AdminComplexPassword';
|
||||
CREATE USER [admin] FOR LOGIN [admin];
|
||||
|
||||
-- Grant permission to alter any login
|
||||
GRANT ALTER ANY LOGIN TO [admin];
|
||||
```
|
||||
|
||||
To learn more about SQL Server's permission system, please visit this [documentation](https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/getting-started-with-database-engine-permissions).
|
||||
|
||||
## How it works
|
||||
|
||||
1. Infisical connects to your database using the provided `admin` login credentials.
|
||||
2. A random value is generated and the password for `user-a` is updated with the new value.
|
||||
3. The new password is then tested by logging into the database.
|
||||
4. If test is successful, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
|
||||
5. The process is then repeated for `user-b` on the next rotation.
|
||||
6. The cycle repeats until secret rotation is deleted/stopped.
|
||||
|
||||
## Rotation Configuration
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Rotation Page">
|
||||
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||
</Step>
|
||||
<Step title="Click on Microsoft SQL Server card" />
|
||||
<Step title="Provide the inputs">
|
||||
<ParamField path="Admin Username" type="string" required>
|
||||
SQL Server admin username
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Admin password" type="string" required>
|
||||
SQL Server admin password
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Host" type="string" required>
|
||||
SQL Server host url (e.g., your-server.database.windows.net)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Port" type="number" required>
|
||||
Database port number (default: 1433)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Database" type="string" required>
|
||||
Database name (default: master)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Username1" type="string" required>
|
||||
The first login name to rotate - `user-a`
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Username2" type="string" required>
|
||||
The second login name to rotate - `user-b`
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="CA" type="string">
|
||||
Optional database certificate to connect with database
|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
<Step title="Configure the output secret mapping">
|
||||
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||
|
||||
<ParamField path="Environment" type="string" required>
|
||||
The environment where the rotated credentials should be mapped to.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Secret Path" type="string" required>
|
||||
The secret path where the rotated credentials should be mapped to.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Interval" type="number" required>
|
||||
What interval should the credentials be rotated in days.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="DB USERNAME" type="string" required>
|
||||
Select an existing secret key where the rotated database username value should be saved to.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="DB PASSWORD" type="string" required>
|
||||
Select an existing select key where the rotated database password value should be saved to.
|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Why can't we delete the other user when rotating?">
|
||||
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
|
||||
|
||||
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
|
||||
|
||||
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Why do you need an admin account?">
|
||||
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
|
||||
|
||||
You don't need to grant all permissions for your admin account but rather just the permission to alter logins (ALTER ANY LOGIN).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="How does this work with Azure SQL Database?">
|
||||
When using Azure SQL Database, you'll need to:
|
||||
|
||||
1. Use the full server name as your host (e.g., your-server.database.windows.net)
|
||||
2. Ensure your admin account is either the Azure SQL Server admin or an Azure AD account with appropriate permissions
|
||||
3. Configure your Azure SQL Server firewall rules to allow connections from Infisical's IP addresses
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
@ -3,81 +3,209 @@ title: "Permissions"
|
||||
description: "Infisical's permissions system provides granular access control."
|
||||
---
|
||||
|
||||
## Summary
|
||||
## Overview
|
||||
|
||||
The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform.
|
||||
|
||||
Permissions are built on a subject-action-object model. The subject is the resource permission is being applied to, the action is what the permission allows.
|
||||
Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows.
|
||||
An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets.
|
||||
|
||||
Currently Infisical supports 4 actions:
|
||||
1. `read`, allows the subject to read the object.
|
||||
2. `create`, allows the subject to create the object.
|
||||
3. `edit`, allows the subject to edit the object.
|
||||
4. `delete`, allows the subject to delete the object.
|
||||
|
||||
Most subjects support all 4 actions, but some subjects only support a subset of actions. Please view the table below for a list of subjects and the actions they support.
|
||||
|
||||
Refer to the table below for a list of subjects and the actions they support.
|
||||
|
||||
## Subjects and Actions
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Project Permissions">
|
||||
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the `secrets-rollback` subject only supports `read`, and `create` as actions. While `secrets` support `read`, `create`, `edit`, `delete`.
|
||||
Not all actions are applicable to all subjects. As an example, the
|
||||
`secrets-rollback` subject only supports `read`, and `create` as actions.
|
||||
While `secrets` support `read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
|-----------------------------|---------|
|
||||
| `secrets` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-approval` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-rotation` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-rollback` | `read`, `create` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `integrations` | `read`, `create`, `edit`, `delete` |
|
||||
| `webhooks` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `service-tokens` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `environments` | `read`, `create`, `edit`, `delete` |
|
||||
| `tags` | `read`, `create`, `edit`, `delete` |
|
||||
| `audit-logs` | `read`, `create`, `edit`, `delete` |
|
||||
| `ip-allowlist` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-authorities` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificates` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-templates` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-alerts` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-collections` | `read`, `create`, `edit`, `delete` |
|
||||
| `workspace` | `edit`, `delete` |
|
||||
| `kms` | `edit` |
|
||||
| Subject | Actions |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `integrations` | `read`, `create`, `edit`, `delete` |
|
||||
| `webhooks` | `read`, `create`, `edit`, `delete` |
|
||||
| `service-tokens` | `read`, `create`, `edit`, `delete` |
|
||||
| `environments` | `read`, `create`, `edit`, `delete` |
|
||||
| `tags` | `read`, `create`, `edit`, `delete` |
|
||||
| `audit-logs` | `read`, `create`, `edit`, `delete` |
|
||||
| `ip-allowlist` | `read`, `create`, `edit`, `delete` |
|
||||
| `workspace` | `edit`, `delete` |
|
||||
| `secrets` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-folders` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-imports` | `read`, `create`, `edit`, `delete` |
|
||||
| `dynamic-secrets` | `read-root-credential`, `create-root-credential`, `edit-root-credential`, `delete-root-credential`, `lease` |
|
||||
| `secret-rollback` | `read`, `create` |
|
||||
| `secret-approval` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-rotation` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-authorities` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificates` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-templates` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-alerts` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-collections` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `edit` |
|
||||
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
|
||||
|
||||
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
|
||||
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
|
||||
</Tab>
|
||||
|
||||
|
||||
<Tab title="Organization Permissions">
|
||||
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the `workspace` subject only supports `read`, and `create` as actions. While `member` support `read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the `workspace`
|
||||
subject only supports `read`, and `create` as actions. While `member` support
|
||||
`read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| `workspace` | `read`, `create` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `read` |
|
||||
|
||||
| Subject | Actions |
|
||||
|-----------------------------|------------------------------------|
|
||||
| `workspace` | `read`, `create` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `read` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tabs>
|
||||
|
||||
## Inversion
|
||||
|
||||
Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
- cmek
|
||||
|
||||
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:
|
||||
|
||||
```typescript
|
||||
// Regular permission - allows reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"]
|
||||
}
|
||||
|
||||
// Inverted permission - denies reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"],
|
||||
inverted: true
|
||||
}
|
||||
```
|
||||
|
||||
## Conditions
|
||||
|
||||
Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
|
||||
### Properties
|
||||
|
||||
Conditions can be applied to the following properties:
|
||||
|
||||
- `environment`: Control access based on environment slugs
|
||||
- `secretPath`: Control access based on secret paths
|
||||
- `secretName`: Control access based on secret names
|
||||
- `secretTags`: Control access based on tags (only supports $in operator)
|
||||
|
||||
### Operators
|
||||
|
||||
The following operators are available for conditions:
|
||||
|
||||
| Operator | Description | Example |
|
||||
| -------- | ---------------------------------- | ----------------------------------------------------- |
|
||||
| `$eq` | Equal | `{ environment: { $eq: "production" } }` |
|
||||
| `$ne` | Not equal | `{ environment: { $ne: "development" } }` |
|
||||
| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` |
|
||||
| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` |
|
||||
|
||||
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
|
||||
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
|
||||
|
||||
## Migrating from permission V1 to permission V2
|
||||
|
||||
When upgrading to V2 permissions (i.e. when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows:
|
||||
|
||||
Any permissions for `secrets` should be expanded to include equivalent permissions for:
|
||||
|
||||
- `secret-imports`
|
||||
- `secret-folders` (except for read permissions)
|
||||
- `dynamic-secrets`
|
||||
|
||||
For dynamic secrets, the actions need to be mapped differently:
|
||||
|
||||
- `read` → `read-root-credential`
|
||||
- `create` → `create-root-credential`
|
||||
- `edit` → `edit-root-credential` (also adds `lease` permission)
|
||||
- `delete` → `delete-root-credential`
|
||||
|
||||
Example:
|
||||
|
||||
```hcl
|
||||
# Old V1 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions = [
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "read"
|
||||
},
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "edit"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# New V2 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions_v2 = [
|
||||
# Original secrets permission
|
||||
{
|
||||
subject = "secrets"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add equivalent secret-imports permission
|
||||
{
|
||||
subject = "secret-imports"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add secret-folders permission (without read)
|
||||
{
|
||||
subject = "secret-folders"
|
||||
action = ["edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add dynamic-secrets permission with mapped actions
|
||||
{
|
||||
subject = "dynamic-secrets"
|
||||
action = ["read-root-credential", "edit-root-credential", "lease"]
|
||||
inverted = false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note: When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions.
|
||||
|
@ -165,6 +165,7 @@
|
||||
"documentation/platform/secret-rotation/sendgrid",
|
||||
"documentation/platform/secret-rotation/postgres",
|
||||
"documentation/platform/secret-rotation/mysql",
|
||||
"documentation/platform/secret-rotation/mssql",
|
||||
"documentation/platform/secret-rotation/aws-iam"
|
||||
]
|
||||
},
|
||||
|
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@ -65,7 +65,6 @@
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"infisical-node": "^1.0.37",
|
||||
"ip": "^2.0.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
@ -15976,11 +15975,6 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
@ -78,7 +78,6 @@
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"infisical-node": "^1.0.37",
|
||||
"ip": "^2.0.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
|
BIN
frontend/public/images/secretRotation/mssqlserver.png
Normal file
BIN
frontend/public/images/secretRotation/mssqlserver.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -1,10 +1,12 @@
|
||||
import { ChangeEventHandler, useState } from "react";
|
||||
import { DayPicker, DayPickerProps } from "react-day-picker";
|
||||
import { faCalendar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover";
|
||||
import { format } from "date-fns";
|
||||
import { format, setHours, setMinutes } from "date-fns";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2";
|
||||
|
||||
export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
||||
@ -22,15 +24,58 @@ export const DatePicker = ({
|
||||
popUpContentProps,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
||||
|
||||
const handleTimeChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const time = e.target.value;
|
||||
if (time) {
|
||||
setTimeValue(time);
|
||||
if (value) {
|
||||
const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10));
|
||||
const newSelectedDate = setHours(setMinutes(value, minutes), hours);
|
||||
onChange(newSelectedDate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDaySelect = (date: Date | undefined) => {
|
||||
if (!timeValue || !date) {
|
||||
onChange(date);
|
||||
return;
|
||||
}
|
||||
|
||||
const [hours, minutes] = timeValue.split(":").map((str) => parseInt(str, 10));
|
||||
const newDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hours, minutes);
|
||||
onChange(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover {...popUpProps}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
||||
{value ? format(value, "PPP") : "Pick a date"}
|
||||
{value ? format(value, "PPP") : "Pick a date and time"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
||||
<DayPicker {...props} mode="single" selected={value} onSelect={onChange} />
|
||||
<div className="mx-4 my-4">
|
||||
<Input
|
||||
type="time"
|
||||
value={timeValue}
|
||||
onChange={handleTimeChange}
|
||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||
/>
|
||||
</div>
|
||||
<DayPicker
|
||||
{...props}
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={handleDaySelect}
|
||||
modifiersStyles={{
|
||||
selected: {
|
||||
background: "#cad62d"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
@ -31,7 +31,8 @@ export const useCreateSecretV3 = ({
|
||||
secretKey,
|
||||
secretValue,
|
||||
secretComment,
|
||||
skipMultilineEncoding
|
||||
skipMultilineEncoding,
|
||||
tagIds
|
||||
}) => {
|
||||
const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, {
|
||||
secretPath,
|
||||
@ -40,7 +41,8 @@ export const useCreateSecretV3 = ({
|
||||
workspaceId,
|
||||
secretValue,
|
||||
secretComment,
|
||||
skipMultilineEncoding
|
||||
skipMultilineEncoding,
|
||||
tagIds
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
@ -132,6 +132,7 @@ export type TCreateSecretsV3DTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
type: SecretType;
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TUpdateSecretsV3DTO = {
|
||||
|
@ -12,7 +12,7 @@ export const AuditLogsPage = withPermission(
|
||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection filterClassName="static p-2" showFilters isOrgAuditLogs />
|
||||
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,11 +1,7 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useEffect, useState } from "react";
|
||||
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faFilterCircleXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@ -20,7 +16,7 @@ import {
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
@ -60,11 +56,15 @@ export const LogsFilter = ({
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
|
||||
const { currentWorkspace, workspaces } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
||||
|
||||
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?.id ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaces.length) {
|
||||
setValue("projectId", workspaces[0].id);
|
||||
if (workspacesInOrg.length) {
|
||||
setValue("projectId", workspacesInOrg[0].id);
|
||||
}
|
||||
}, [workspaces]);
|
||||
|
||||
@ -130,7 +130,7 @@ export const LogsFilter = ({
|
||||
: selectedEventTypes?.length === 0
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faChevronDown} className="ml-2 text-xs" />
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="z-[100] max-h-80 overflow-hidden">
|
||||
@ -221,10 +221,7 @@ export const LogsFilter = ({
|
||||
if (e === "all") onChange(undefined);
|
||||
else onChange(e);
|
||||
}}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
value === undefined && "text-mineshaft-400"
|
||||
)}
|
||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
@ -239,7 +236,7 @@ export const LogsFilter = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{isOrgAuditLogs && workspaces.length > 0 && (
|
||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectId"
|
||||
@ -255,11 +252,11 @@ export const LogsFilter = ({
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 ",
|
||||
value === undefined && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
{workspaces.map((project) => (
|
||||
{workspacesInOrg.map((project) => (
|
||||
<SelectItem value={String(project.id || "")} key={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
@ -277,10 +274,7 @@ export const LogsFilter = ({
|
||||
<FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
setIsStartDatePickerOpen(false);
|
||||
}}
|
||||
onChange={onChange}
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
@ -299,11 +293,7 @@ export const LogsFilter = ({
|
||||
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(pickedDate) => {
|
||||
pickedDate?.setHours(23, 59, 59, 999); // we choose the end of today not the start of it (going off of aws cloud watch)
|
||||
onChange(pickedDate);
|
||||
setIsEndDatePickerOpen(false);
|
||||
}}
|
||||
onChange={onChange}
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
|
@ -88,7 +88,7 @@ export const LogsSection = ({
|
||||
refetchInterval={refetchInterval}
|
||||
remappedHeaders={remappedHeaders}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn && !isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn}
|
||||
filter={{
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
projectId,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Badge, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Td, Tr } from "@app/components/v2";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
|
||||
import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
type Props = {
|
||||
auditLog: AuditLog;
|
||||
@ -11,6 +11,10 @@ type Props = {
|
||||
|
||||
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||
const renderActor = (actor: Actor) => {
|
||||
if (!actor) {
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
switch (actor.type) {
|
||||
case ActorType.USER:
|
||||
return (
|
||||
@ -38,491 +42,6 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
}
|
||||
};
|
||||
|
||||
const renderMetadata = (event: Event) => {
|
||||
const metadataKeys = Object.keys(event.metadata);
|
||||
|
||||
switch (event.type) {
|
||||
case EventType.GET_SECRETS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`# Secrets: ${event.metadata.numberOfSecrets}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.AUTHORIZE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UNAUTHORIZE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
{event.metadata.app && <p>{`Target app: ${event.metadata.app}`}</p>}
|
||||
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
|
||||
{event.metadata.targetEnvironment && (
|
||||
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
|
||||
)}
|
||||
{event.metadata.targetEnvironmentId && (
|
||||
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
{event.metadata.app && <p>{`Target App: ${event.metadata.app}`}</p>}
|
||||
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
|
||||
{event.metadata.targetEnvironment && (
|
||||
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
|
||||
)}
|
||||
{event.metadata.targetEnvironmentId && (
|
||||
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SERVICE_TOKEN:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SERVICE_TOKEN:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Slug: ${event.metadata.slug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Old name: ${event.metadata.oldName}`}</p>
|
||||
<p>{`New name: ${event.metadata.newName}`}</p>
|
||||
<p>{`Old slug: ${event.metadata.oldSlug}`}</p>
|
||||
<p>{`New slug: ${event.metadata.newSlug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Slug: ${event.metadata.slug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_WORKSPACE_MEMBER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.REMOVE_WORKSPACE_MEMBER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Folder: ${event.metadata.folderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Old folder: ${event.metadata.oldFolderName}`}</p>
|
||||
<p>{`New folder: ${event.metadata.newFolderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Folder: ${event.metadata.folderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_WEBHOOK:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_WEBHOOK_STATUS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_WEBHOOK:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_SECRET_IMPORTS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`# Imported paths: ${event.metadata.numberOfImports}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
|
||||
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
|
||||
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_USER_WORKSPACE_ROLE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
<p>{`Old role: ${event.metadata.oldRole}`}</p>
|
||||
<p>{`New role: ${event.metadata.newRole}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
{event.metadata.deniedPermissions.map((permission) => {
|
||||
return (
|
||||
<p
|
||||
key={`audit-log-denied-permission-${event.metadata.userId}-${permission.environmentSlug}-${permission.ability}`}
|
||||
>
|
||||
{`Denied env-ability: ${permission.environmentSlug}-${permission.ability}`}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
case EventType.ORG_ADMIN_ACCESS_PROJECT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_PKI_ALERT:
|
||||
case EventType.UPDATE_PKI_ALERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Alert ID: ${event.metadata.pkiAlertId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Alert Before Days: ${event.metadata.alertBeforeDays}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_ALERT:
|
||||
case EventType.DELETE_PKI_ALERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Alert ID: ${event.metadata.pkiAlertId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_COLLECTION:
|
||||
case EventType.DELETE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_COLLECTION_ITEMS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_PKI_COLLECTION_ITEM:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Collection Item ID: ${event.metadata.pkiCollectionItemId}`}</p>
|
||||
<p>{`Type: ${event.metadata.type}`}</p>
|
||||
<p>{`Item ID: ${event.metadata.itemId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_PKI_COLLECTION_ITEM:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Collection Item ID: ${event.metadata.pkiCollectionItemId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CA:
|
||||
case EventType.GET_CA:
|
||||
case EventType.UPDATE_CA:
|
||||
case EventType.DELETE_CA:
|
||||
case EventType.GET_CA_CSR:
|
||||
case EventType.GET_CA_CERT:
|
||||
case EventType.IMPORT_CA_CERT:
|
||||
case EventType.GET_CA_CRL:
|
||||
case EventType.SIGN_INTERMEDIATE:
|
||||
case EventType.ISSUE_CERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`CA DN: ${event.metadata.dn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERT:
|
||||
case EventType.DELETE_CERT:
|
||||
case EventType.REVOKE_CERT:
|
||||
case EventType.GET_CERT_BODY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Cert CN: ${event.metadata.cn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Certificate Authority ID: ${event.metadata.caId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Common Name: ${event.metadata.commonName}`}</p>
|
||||
<p>{`Subject Alternative Name: ${event.metadata.subjectAlternativeName}`}</p>
|
||||
<p>{`TTL: ${event.metadata.ttl}`}</p>
|
||||
{event.metadata.pkiCollectionId && (
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE:
|
||||
case EventType.DELETE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Enabled: ${event.metadata.isEnabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PROJECT_SLACK_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Project Slack Config ID: ${event.metadata.id}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_PROJECT_SLACK_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Project Slack Config ID: ${event.metadata.id}`}</p>
|
||||
<p>{`Slack integration ID: ${event.metadata.slackIntegrationId}`}</p>
|
||||
<p>{`Access Request Notification Status: ${event.metadata.isAccessRequestNotificationEnabled}`}</p>
|
||||
<p>{`Access Request Channels: ${event.metadata.accessRequestChannels}`}</p>
|
||||
<p>{`Secret Approval Request Notification Status: ${event.metadata.isSecretRequestNotificationEnabled}`}</p>
|
||||
<p>{`Secret Request Channels: ${event.metadata.secretRequestChannels}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.INTEGRATION_SYNCED:
|
||||
return (
|
||||
<Td>
|
||||
<Tooltip
|
||||
className="max-w-xs whitespace-normal break-words"
|
||||
content={event.metadata.syncMessage!}
|
||||
isDisabled={!event.metadata.syncMessage}
|
||||
>
|
||||
<Badge variant={event.metadata.isSynced ? "success" : "danger"}>
|
||||
<p className="text-center">{event.metadata.isSynced ? "Successful" : "Failed"}</p>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.GET_WORKSPACE_KEY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Key ID: ${event.metadata.keyId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.ADD_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Identity ID: ${event.metadata.identityId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET:
|
||||
case EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Identity ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Client Secret ID: ${event.metadata.clientSecretId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
// ? If for some reason non the above events are matched, we will display the first 3 metadata items in the metadata object.
|
||||
default:
|
||||
if (metadataKeys.length) {
|
||||
const maxMetadataLength = metadataKeys.length > 3 ? 3 : metadataKeys.length;
|
||||
return (
|
||||
<Td>
|
||||
{Object.entries(event.metadata)
|
||||
.slice(0, maxMetadataLength)
|
||||
.map(([key, value]) => {
|
||||
return <p key={`audit-log-metadata-${key}`}>{`${key}: ${value}`}</p>;
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
return <Td />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateToFormat: string) => {
|
||||
const date = new Date(dateToFormat);
|
||||
const year = date.getFullYear();
|
||||
@ -576,7 +95,7 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
{isOrgAuditLogs && <Td>{auditLog?.projectName ?? auditLog?.projectId ?? "N/A"}</Td>}
|
||||
{showActorColumn && renderActor(auditLog.actor)}
|
||||
{renderSource()}
|
||||
{renderMetadata(auditLog.event)}
|
||||
<Td className="max-w-xs break-all">{JSON.stringify(auditLog.event.metadata || {})}</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
@ -96,7 +96,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
<SecretInput isReadOnly value={secretVersion?.secretValue} />
|
||||
</Td>
|
||||
<Td>{secretVersion?.secretComment}</Td>
|
||||
<Td>
|
||||
<Td className="flex flex-wrap gap-2">
|
||||
{secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
@ -118,7 +118,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
<SecretInput isReadOnly value={newVersion?.secretValue} />
|
||||
</Td>
|
||||
<Td>{newVersion?.secretComment}</Td>
|
||||
<Td>
|
||||
<Td className="flex flex-wrap gap-2">
|
||||
{newVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
|
@ -9,7 +9,14 @@ import { twMerge } from "tailwind-merge";
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||
import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
Checkbox,
|
||||
ContentLoader,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Pagination,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
@ -41,7 +48,10 @@ import { SecretDropzone } from "./components/SecretDropzone";
|
||||
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
|
||||
import { SnapshotView } from "./components/SnapshotView";
|
||||
import {
|
||||
PopUpNames,
|
||||
StoreProvider,
|
||||
usePopUpAction,
|
||||
usePopUpState,
|
||||
useSelectedSecretActions,
|
||||
useSelectedSecrets
|
||||
} from "./SecretMainPage.store";
|
||||
@ -123,6 +133,9 @@ const SecretMainPageContent = () => {
|
||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
||||
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
||||
|
||||
const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
const { togglePopUp } = usePopUpAction();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isWorkspaceLoading &&
|
||||
@ -520,13 +533,24 @@ const SecretMainPageContent = () => {
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
<CreateSecretForm
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
autoCapitalize={currentWorkspace?.autoCapitalization}
|
||||
isProtectedBranch={isProtectedBranch}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={createSecretPopUp.isOpen}
|
||||
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create Secret"
|
||||
subTitle="Add a secret to this particular environment and folder"
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<CreateSecretForm
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
autoCapitalize={currentWorkspace?.autoCapitalization}
|
||||
isProtectedBranch={isProtectedBranch}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<SecretDropzone
|
||||
secrets={secrets}
|
||||
environment={environment}
|
||||
|
@ -1,20 +1,24 @@
|
||||
import { ClipboardEvent } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, MultiSelect } from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateSecretV3 } from "@app/hooks/api";
|
||||
import { useCreateSecretV3, useGetWsTags } from "@app/hooks/api";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store";
|
||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||
|
||||
const typeSchema = z.object({
|
||||
key: z.string().trim().min(1, { message: "Secret key is required" }),
|
||||
value: z.string().optional()
|
||||
value: z.string().optional(),
|
||||
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof typeSchema>;
|
||||
@ -43,12 +47,16 @@ export const CreateSecretForm = ({
|
||||
setValue,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
const { closePopUp, togglePopUp } = usePopUpAction();
|
||||
const { closePopUp } = usePopUpAction();
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { permission } = useProjectPermission();
|
||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
||||
canReadTags ? workspaceId : ""
|
||||
);
|
||||
|
||||
const handleFormSubmit = async ({ key, value }: TFormSchema) => {
|
||||
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
|
||||
try {
|
||||
await createSecretV3({
|
||||
environment,
|
||||
@ -57,7 +65,8 @@ export const CreateSecretForm = ({
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
type: SecretType.Shared,
|
||||
tagIds: tags?.map((el) => el.value)
|
||||
});
|
||||
closePopUp(PopUpNames.CreateSecretForm);
|
||||
reset();
|
||||
@ -88,67 +97,90 @@ export const CreateSecretForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create secret"
|
||||
subTitle="Add a secret to the particular environment and folder"
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={autoCapitalize}
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={autoCapitalize}
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Tags"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
helperText={
|
||||
!canReadTags ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
|
||||
<span>You do not have permission to read tags.</span>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
>
|
||||
<MultiSelect
|
||||
className="w-full"
|
||||
placeholder="Select tags to assign to secret..."
|
||||
isMulti
|
||||
name="tagIds"
|
||||
isDisabled={!canReadTags}
|
||||
isLoading={isTagsLoading}
|
||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -1116,13 +1116,23 @@ export const SecretOverviewPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CreateSecretForm
|
||||
secretPath={secretPath}
|
||||
<Modal
|
||||
isOpen={popUp.addSecretsInAllEnvs.isOpen}
|
||||
getSecretByKey={getSecretByKey}
|
||||
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
||||
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
||||
/>
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
className="max-h-[80vh]"
|
||||
bodyClassName="overflow-visible"
|
||||
title="Create Secrets"
|
||||
subTitle="Create a secret across multiple environments"
|
||||
>
|
||||
<CreateSecretForm
|
||||
secretPath={secretPath}
|
||||
getSecretByKey={getSecretByKey}
|
||||
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={popUp.addFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ClipboardEvent } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@ -13,8 +13,7 @@ import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
MultiSelect,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
@ -25,14 +24,20 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import {
|
||||
useCreateFolder,
|
||||
useCreateSecretV3,
|
||||
useGetWsTags,
|
||||
useUpdateSecretV3
|
||||
} from "@app/hooks/api";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
|
||||
const typeSchema = z
|
||||
.object({
|
||||
key: z.string().trim().min(1, "Key is required"),
|
||||
value: z.string().optional(),
|
||||
environments: z.record(z.boolean().optional())
|
||||
environments: z.record(z.boolean().optional()),
|
||||
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
|
||||
})
|
||||
.refine((data) => data.key !== undefined, {
|
||||
message: "Please enter secret name"
|
||||
@ -44,18 +49,10 @@ type Props = {
|
||||
secretPath?: string;
|
||||
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
|
||||
// modal props
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
onTogglePopUp: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const CreateSecretForm = ({
|
||||
secretPath = "/",
|
||||
isOpen,
|
||||
getSecretByKey,
|
||||
onClose,
|
||||
onTogglePopUp
|
||||
}: Props) => {
|
||||
export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: Props) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -69,14 +66,18 @@ export const CreateSecretForm = ({
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { permission } = useProjectPermission();
|
||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
||||
canReadTags ? workspaceId : ""
|
||||
);
|
||||
|
||||
const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => {
|
||||
const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => {
|
||||
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
|
||||
const isEnvironmentsSelected = environmentsSelected.length;
|
||||
|
||||
@ -120,7 +121,8 @@ export const CreateSecretForm = ({
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
type: SecretType.Shared
|
||||
type: SecretType.Shared,
|
||||
tagIds: tags?.map((el) => el.value)
|
||||
})),
|
||||
environment
|
||||
};
|
||||
@ -134,7 +136,8 @@ export const CreateSecretForm = ({
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
type: SecretType.Shared,
|
||||
tagIds: tags?.map((el) => el.value)
|
||||
})),
|
||||
environment
|
||||
};
|
||||
@ -197,114 +200,136 @@ export const CreateSecretForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||
<ModalContent
|
||||
className="max-h-[80vh] overflow-y-auto"
|
||||
title="Bulk Create & Update"
|
||||
subTitle="Create & update a secret across many environments"
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Environments" className="mb-2" />
|
||||
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
||||
{environments
|
||||
.filter((environmentSlug) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: environmentSlug.slug,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Tags"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
helperText={
|
||||
!canReadTags ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
|
||||
<span>You do not have permission to read tags.</span>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
.map((env) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`environments.${env.slug}`}
|
||||
key={`secret-input-${env.slug}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`secret-input-${env.slug}`}
|
||||
className="!justify-start"
|
||||
>
|
||||
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
||||
<span title={env.name} className="truncate">
|
||||
{env.name}
|
||||
</span>
|
||||
<span>
|
||||
{getSecretByKey(env.slug, newSecretKey) && (
|
||||
<Tooltip
|
||||
className="max-w-[150px]"
|
||||
content="Secret already exists, and it will be overwritten"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faWarning}
|
||||
className="ml-1 text-yellow-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={onClose}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
}
|
||||
>
|
||||
<MultiSelect
|
||||
className="w-full"
|
||||
placeholder="Select tags to assign to secrets..."
|
||||
isMulti
|
||||
name="tagIds"
|
||||
isDisabled={!canReadTags}
|
||||
isLoading={isTagsLoading}
|
||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Environments" className="mb-2" />
|
||||
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
||||
{environments
|
||||
.filter((environmentSlug) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: environmentSlug.slug,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})
|
||||
)
|
||||
)
|
||||
.map((env) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`environments.${env.slug}`}
|
||||
key={`secret-input-${env.slug}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`secret-input-${env.slug}`}
|
||||
className="!justify-start"
|
||||
>
|
||||
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
||||
<span title={env.name} className="truncate">
|
||||
{env.name}
|
||||
</span>
|
||||
<span>
|
||||
{getSecretByKey(env.slug, newSecretKey) && (
|
||||
<Tooltip
|
||||
className="max-w-[150px]"
|
||||
content="Secret already exists, and it will be overwritten"
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={onClose}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -1,3 +1,13 @@
|
||||
## 1.4.0 (November 06, 2024)
|
||||
|
||||
Changes:
|
||||
* Chart is now fully documented
|
||||
* New fields introduced: `infisical.databaseSchemaMigrationJob.image` and `infisical.serviceAccount`
|
||||
|
||||
Features:
|
||||
|
||||
* Added support for auto creating service account with required permissions via `infisical.serviceAccount.create`
|
||||
|
||||
## 1.3.0 (October 28, 2024)
|
||||
|
||||
Changes:
|
||||
|
@ -1,13 +1,13 @@
|
||||
apiVersion: v2
|
||||
name: infisical-standalone
|
||||
description: A helm chart for a full Infisical application
|
||||
description: A helm chart to deploy Infisical
|
||||
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.3.0
|
||||
version: 1.4.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
|
66
helm-charts/infisical-standalone-postgres/README.md
Normal file
66
helm-charts/infisical-standalone-postgres/README.md
Normal file
@ -0,0 +1,66 @@
|
||||
# infisical-standalone
|
||||
|
||||
  
|
||||
|
||||
A helm chart to deploy Infisical
|
||||
|
||||
## Requirements
|
||||
|
||||
| Repository | Name | Version |
|
||||
|------------|------|---------|
|
||||
| https://charts.bitnami.com/bitnami | postgresql | 14.1.3 |
|
||||
| https://charts.bitnami.com/bitnami | redis | 18.14.0 |
|
||||
| https://kubernetes.github.io/ingress-nginx | ingress-nginx | 4.0.13 |
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| fullnameOverride | string | `""` | Overrides the full name of the release, affecting resource names |
|
||||
| infisical.affinity | object | `{}` | Node affinity settings for pod placement |
|
||||
| infisical.autoDatabaseSchemaMigration | bool | `true` | Automatically migrates new database schema when deploying |
|
||||
| infisical.databaseSchemaMigrationJob.image.pullPolicy | string | `"IfNotPresent"` | Pulls image only if not present on the node |
|
||||
| infisical.databaseSchemaMigrationJob.image.repository | string | `"ghcr.io/groundnuty/k8s-wait-for"` | Image repository for migration wait job |
|
||||
| infisical.databaseSchemaMigrationJob.image.tag | string | `"no-root-v2.0"` | Image tag version |
|
||||
| infisical.deploymentAnnotations | object | `{}` | Custom annotations for Infisical deployment |
|
||||
| infisical.enabled | bool | `true` | |
|
||||
| infisical.fullnameOverride | string | `""` | Override for the full name of Infisical resources in this deployment |
|
||||
| infisical.image.imagePullSecrets | list | `[]` | Secret references for pulling the image, if needed |
|
||||
| infisical.image.pullPolicy | string | `"IfNotPresent"` | Pulls image only if not already present on the node |
|
||||
| infisical.image.repository | string | `"infisical/infisical"` | Image repository for the Infisical service |
|
||||
| infisical.image.tag | string | `"v0.93.1-postgres"` | Specific version tag of the Infisical image. View the latest version here https://hub.docker.com/r/infisical/infisical |
|
||||
| infisical.kubeSecretRef | string | `"infisical-secrets"` | Kubernetes Secret reference containing Infisical root credentials |
|
||||
| infisical.name | string | `"infisical"` | |
|
||||
| infisical.podAnnotations | object | `{}` | Custom annotations for Infisical pods |
|
||||
| infisical.replicaCount | int | `2` | Number of pod replicas for high availability |
|
||||
| infisical.resources.limits.memory | string | `"600Mi"` | Memory limit for Infisical container |
|
||||
| infisical.resources.requests.cpu | string | `"350m"` | CPU request for Infisical container |
|
||||
| infisical.service.annotations | object | `{}` | Custom annotations for Infisical service |
|
||||
| infisical.service.nodePort | string | `""` | Optional node port for service when using NodePort type |
|
||||
| infisical.service.type | string | `"ClusterIP"` | Service type, can be changed based on exposure needs (e.g., LoadBalancer) |
|
||||
| infisical.serviceAccount.annotations | object | `{}` | Custom annotations for the auto-created service account |
|
||||
| infisical.serviceAccount.create | bool | `true` | Creates a new service account if true, with necessary permissions for this chart. If false and `serviceAccount.name` is not defined, the chart will attempt to use the Default service account |
|
||||
| infisical.serviceAccount.name | string | `nil` | Optional custom service account name, if existing service account is used |
|
||||
| ingress.annotations | object | `{}` | Custom annotations for ingress resource |
|
||||
| ingress.enabled | bool | `true` | Enable or disable ingress configuration |
|
||||
| ingress.hostName | string | `""` | Hostname for ingress access, e.g., app.example.com |
|
||||
| ingress.ingressClassName | string | `"nginx"` | Specifies the ingress class, useful for multi-ingress setups |
|
||||
| ingress.nginx.enabled | bool | `true` | Enable NGINX-specific settings, if using NGINX ingress controller |
|
||||
| ingress.tls | list | `[]` | TLS settings for HTTPS access |
|
||||
| nameOverride | string | `""` | Overrides the default release name |
|
||||
| postgresql.auth.database | string | `"infisicalDB"` | Database name for Infisical |
|
||||
| postgresql.auth.password | string | `"root"` | Password for PostgreSQL database access |
|
||||
| postgresql.auth.username | string | `"infisical"` | Database username for PostgreSQL |
|
||||
| postgresql.enabled | bool | `true` | Enables an in-cluster PostgreSQL deployment. To achieve HA for Postgres, we recommend deploying https://github.com/zalando/postgres-operator instead. |
|
||||
| postgresql.fullnameOverride | string | `"postgresql"` | Full name override for PostgreSQL resources |
|
||||
| postgresql.name | string | `"postgresql"` | PostgreSQL resource name |
|
||||
| postgresql.useExistingPostgresSecret.enabled | bool | `false` | Set to true if using an existing Kubernetes secret that contains PostgreSQL connection string |
|
||||
| postgresql.useExistingPostgresSecret.existingConnectionStringSecret.key | string | `""` | Key name in the Kubernetes secret that holds the connection string |
|
||||
| postgresql.useExistingPostgresSecret.existingConnectionStringSecret.name | string | `""` | Kubernetes secret name containing the PostgreSQL connection string |
|
||||
| redis.architecture | string | `"standalone"` | Redis deployment type (e.g., standalone or cluster) |
|
||||
| redis.auth.password | string | `"mysecretpassword"` | Redis password |
|
||||
| redis.cluster.enabled | bool | `false` | Clustered Redis deployment |
|
||||
| redis.enabled | bool | `true` | Enables an in-cluster Redis deployment |
|
||||
| redis.fullnameOverride | string | `"redis"` | Full name override for Redis resources |
|
||||
| redis.name | string | `"redis"` | Redis resource name |
|
||||
| redis.usePassword | bool | `true` | Requires a password for Redis authentication |
|
@ -40,6 +40,23 @@ component: {{ .Values.infisical.name | quote }}
|
||||
{{ include "infisical.common.matchLabels" . }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.roleName" -}}
|
||||
{{- printf "%s-infisical" .Release.Name -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.roleBindingName" -}}
|
||||
{{- printf "%s-infisical" .Release.Name -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.serviceAccountName" -}}
|
||||
{{- if .Values.infisical.serviceAccount.create -}}
|
||||
{{- printf "%s-infisical" .Release.Name -}}
|
||||
{{- else -}}
|
||||
{{- .Values.infisical.serviceAccount.name | default "default" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{/*
|
||||
Create a fully qualified backend name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
|
@ -34,10 +34,11 @@ spec:
|
||||
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- if $infisicalValues.autoDatabaseSchemaMigration }}
|
||||
serviceAccountName: {{ include "infisical.serviceAccountName" . }}
|
||||
initContainers:
|
||||
- name: "migration-init"
|
||||
image: {{ $infisicalValues.databaseSchemaMigrationInitContainer.image }}
|
||||
imagePullPolicy: {{ $infisicalValues.databaseSchemaMigrationInitContainer.imagePullPolicy }}
|
||||
image: "{{ $infisicalValues.databaseSchemaMigrationJob.image.repository }}:{{ $infisicalValues.databaseSchemaMigrationJob.image.tag }}"
|
||||
imagePullPolicy: {{ $infisicalValues.databaseSchemaMigrationJob.image.pullPolicy }}
|
||||
args:
|
||||
- "job"
|
||||
- "{{ .Release.Name }}-schema-migration-{{ .Release.Revision }}"
|
||||
|
@ -1,8 +1,25 @@
|
||||
---
|
||||
{{- if .Values.infisical.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ printf "%s-infisical" .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "infisical.labels" . | nindent 4 }}
|
||||
{{- with .Values.infisical.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: k8s-wait-for-infisical-schema-migration
|
||||
name: {{ include "infisical.roleName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "infisical.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
@ -11,13 +28,15 @@ rules:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: infisical-database-schema-migration
|
||||
name: {{ include "infisical.roleBindingName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "infisical.labels" . | nindent 4 }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "infisical.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: k8s-wait-for-infisical-schema-migration
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: {{ include "infisical.roleName" . }}
|
@ -16,7 +16,7 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name | quote }}
|
||||
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
spec:
|
||||
serviceAccountName: {{ .Values.infisical.databaseSchemaMigrationJob.serviceAccountName | default "default" }}
|
||||
serviceAccountName: {{ include "infisical.serviceAccountName" . }}
|
||||
{{- if $infisicalValues.image.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
|
||||
|
@ -1,81 +1,139 @@
|
||||
# -- Overrides the default release name
|
||||
nameOverride: ""
|
||||
|
||||
# -- Overrides the full name of the release, affecting resource names
|
||||
fullnameOverride: ""
|
||||
|
||||
infisical:
|
||||
enabled: true
|
||||
name: infisical
|
||||
enabled: true # -- Enable Infisical chart deployment
|
||||
name: infisical # -- Sets the name of the deployment within this chart
|
||||
|
||||
# -- Automatically migrates new database schema when deploying
|
||||
autoDatabaseSchemaMigration: true
|
||||
databaseSchemaMigrationInitContainer:
|
||||
image: "ghcr.io/groundnuty/k8s-wait-for:no-root-v2.0"
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
databaseSchemaMigrationJob:
|
||||
serviceAccountName: default
|
||||
|
||||
image:
|
||||
# -- Image repository for migration wait job
|
||||
repository: ghcr.io/groundnuty/k8s-wait-for
|
||||
# -- Image tag version
|
||||
tag: no-root-v2.0
|
||||
# -- Pulls image only if not present on the node
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
serviceAccount:
|
||||
# -- Creates a new service account if true, with necessary permissions for this chart. If false and `serviceAccount.name` is not defined, the chart will attempt to use the Default service account
|
||||
create: true
|
||||
# -- Custom annotations for the auto-created service account
|
||||
annotations: {}
|
||||
# -- Optional custom service account name, if existing service account is used
|
||||
name: null
|
||||
|
||||
# -- Override for the full name of Infisical resources in this deployment
|
||||
fullnameOverride: ""
|
||||
# -- Custom annotations for Infisical pods
|
||||
podAnnotations: {}
|
||||
# -- Custom annotations for Infisical deployment
|
||||
deploymentAnnotations: {}
|
||||
# -- Number of pod replicas for high availability
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
# -- Image repository for the Infisical service
|
||||
repository: infisical/infisical
|
||||
tag: "v0.46.3-postgres"
|
||||
# -- Specific version tag of the Infisical image. View the latest version here https://hub.docker.com/r/infisical/infisical
|
||||
tag: "v0.93.1-postgres"
|
||||
# -- Pulls image only if not already present on the node
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Secret references for pulling the image, if needed
|
||||
imagePullSecrets: []
|
||||
|
||||
# -- Node affinity settings for pod placement
|
||||
affinity: {}
|
||||
# -- Kubernetes Secret reference containing Infisical root credentials
|
||||
kubeSecretRef: "infisical-secrets"
|
||||
|
||||
service:
|
||||
# -- Custom annotations for Infisical service
|
||||
annotations: {}
|
||||
# -- Service type, can be changed based on exposure needs (e.g., LoadBalancer)
|
||||
type: ClusterIP
|
||||
# -- Optional node port for service when using NodePort type
|
||||
nodePort: ""
|
||||
|
||||
resources:
|
||||
limits:
|
||||
# -- Memory limit for Infisical container
|
||||
memory: 600Mi
|
||||
requests:
|
||||
# -- CPU request for Infisical container
|
||||
cpu: 350m
|
||||
|
||||
ingress:
|
||||
# -- Enable or disable ingress configuration
|
||||
enabled: true
|
||||
# -- Hostname for ingress access, e.g., app.example.com
|
||||
hostName: ""
|
||||
# -- Specifies the ingress class, useful for multi-ingress setups
|
||||
ingressClassName: nginx
|
||||
|
||||
nginx:
|
||||
# -- Enable NGINX-specific settings, if using NGINX ingress controller
|
||||
enabled: true
|
||||
|
||||
# -- Custom annotations for ingress resource
|
||||
annotations: {}
|
||||
# -- TLS settings for HTTPS access
|
||||
tls:
|
||||
[]
|
||||
# -- TLS secret name for HTTPS
|
||||
# - secretName: letsencrypt-prod
|
||||
# -- Domain name to associate with the TLS certificate
|
||||
# hosts:
|
||||
# - some.domain.com
|
||||
|
||||
postgresql:
|
||||
# -- When enabled, this will start up a in cluster Postgres
|
||||
# -- Enables an in-cluster PostgreSQL deployment. To achieve HA for Postgres, we recommend deploying https://github.com/zalando/postgres-operator instead.
|
||||
enabled: true
|
||||
# -- PostgreSQL resource name
|
||||
name: "postgresql"
|
||||
# -- Full name override for PostgreSQL resources
|
||||
fullnameOverride: "postgresql"
|
||||
|
||||
auth:
|
||||
# -- Database username for PostgreSQL
|
||||
username: infisical
|
||||
# -- Password for PostgreSQL database access
|
||||
password: root
|
||||
# -- Database name for Infisical
|
||||
database: infisicalDB
|
||||
|
||||
useExistingPostgresSecret:
|
||||
# -- When this is enabled, postgresql.enabled needs to be false
|
||||
# -- Set to true if using an existing Kubernetes secret that contains PostgreSQL connection string
|
||||
enabled: false
|
||||
# -- The name from where to get the existing postgresql connection string
|
||||
existingConnectionStringSecret:
|
||||
# -- The name of the secret that contains the postgres connection string
|
||||
# -- Kubernetes secret name containing the PostgreSQL connection string
|
||||
name: ""
|
||||
# -- Secret key name that contains the postgres connection string
|
||||
# -- Key name in the Kubernetes secret that holds the connection string
|
||||
key: ""
|
||||
|
||||
redis:
|
||||
# -- Enables an in-cluster Redis deployment
|
||||
enabled: true
|
||||
# -- Redis resource name
|
||||
name: "redis"
|
||||
# -- Full name override for Redis resources
|
||||
fullnameOverride: "redis"
|
||||
|
||||
cluster:
|
||||
# -- Clustered Redis deployment
|
||||
enabled: false
|
||||
|
||||
# -- Requires a password for Redis authentication
|
||||
usePassword: true
|
||||
|
||||
auth:
|
||||
# -- Redis password
|
||||
password: "mysecretpassword"
|
||||
|
||||
# -- Redis deployment type (e.g., standalone or cluster)
|
||||
architecture: standalone
|
||||
|
Reference in New Issue
Block a user