Merge remote-tracking branch 'origin/main' into feature/secrets-detection-in-secrets-manager

This commit is contained in:
Sheen Capadngan
2025-07-29 04:57:54 +08:00
94 changed files with 1949 additions and 612 deletions
+11 -2
View File
@@ -123,8 +123,17 @@ INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET=
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
# azure app connection # azure app connection
INF_APP_CONNECTION_AZURE_CLIENT_ID= INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID=
INF_APP_CONNECTION_AZURE_CLIENT_SECRET= INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET=
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID=
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET=
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID=
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET=
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID=
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET=
# datadog # datadog
SHOULD_USE_DATADOG_TRACER= SHOULD_USE_DATADOG_TRACER=
+6 -3
View File
@@ -145,7 +145,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \ && cd openssl-3.1.2 \
&& ./Configure enable-fips \ && ./Configure enable-fips \
&& make \ && make \
&& make install_fips && make install_fips \
&& cd / \
&& rm -rf /openssl-build \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install Infisical CLI # Install Infisical CLI
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
@@ -186,12 +190,11 @@ ENV NODE_ENV production
ENV STANDALONE_BUILD true ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true ENV STANDALONE_MODE true
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
ENV NODE_OPTIONS="--max-old-space-size=1024" ENV NODE_OPTIONS="--max-old-space-size=8192 --force-fips"
# FIPS mode of operation: # FIPS mode of operation:
ENV OPENSSL_CONF=/backend/nodejs.fips.cnf ENV OPENSSL_CONF=/backend/nodejs.fips.cnf
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
ENV NODE_OPTIONS=--force-fips
ENV FIPS_ENABLED=true ENV FIPS_ENABLED=true
+5 -1
View File
@@ -59,7 +59,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \ && cd openssl-3.1.2 \
&& ./Configure enable-fips \ && ./Configure enable-fips \
&& make \ && make \
&& make install_fips && make install_fips \
&& cd / \
&& rm -rf /openssl-build \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# ? App setup # ? App setup
+18 -13
View File
@@ -7,6 +7,7 @@
"": { "": {
"name": "backend", "name": "backend",
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@aws-sdk/client-elasticache": "^3.637.0", "@aws-sdk/client-elasticache": "^3.637.0",
@@ -61,7 +62,7 @@
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1553.0", "aws-sdk": "^2.1553.0",
"axios": "^1.6.7", "axios": "^1.11.0",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"botbuilder": "^4.23.2", "botbuilder": "^4.23.2",
@@ -13699,14 +13700,16 @@
} }
}, },
"node_modules/@types/request/node_modules/form-data": { "node_modules/@types/request/node_modules/form-data": {
"version": "2.5.2", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
"integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.6", "combined-stream": "^1.0.8",
"mime-types": "^2.1.12", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35",
"safe-buffer": "^5.2.1" "safe-buffer": "^5.2.1"
}, },
"engines": { "engines": {
@@ -15230,13 +15233,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.9", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -18761,13 +18764,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
+1 -1
View File
@@ -181,7 +181,7 @@
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1553.0", "aws-sdk": "^2.1553.0",
"axios": "^1.6.7", "axios": "^1.11.0",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"botbuilder": "^4.23.2", "botbuilder": "^4.23.2",
+21
View File
@@ -489,6 +489,11 @@ import {
TWorkflowIntegrationsInsert, TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate TWorkflowIntegrationsUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import {
TAccessApprovalPoliciesEnvironments,
TAccessApprovalPoliciesEnvironmentsInsert,
TAccessApprovalPoliciesEnvironmentsUpdate
} from "@app/db/schemas/access-approval-policies-environments";
import { import {
TIdentityLdapAuths, TIdentityLdapAuths,
TIdentityLdapAuthsInsert, TIdentityLdapAuthsInsert,
@@ -510,6 +515,11 @@ import {
TRemindersRecipientsInsert, TRemindersRecipientsInsert,
TRemindersRecipientsUpdate TRemindersRecipientsUpdate
} from "@app/db/schemas/reminders-recipients"; } from "@app/db/schemas/reminders-recipients";
import {
TSecretApprovalPoliciesEnvironments,
TSecretApprovalPoliciesEnvironmentsInsert,
TSecretApprovalPoliciesEnvironmentsUpdate
} from "@app/db/schemas/secret-approval-policies-environments";
import { import {
TSecretReminderRecipients, TSecretReminderRecipients,
TSecretReminderRecipientsInsert, TSecretReminderRecipientsInsert,
@@ -887,6 +897,12 @@ declare module "knex/types/tables" {
TAccessApprovalPoliciesBypassersUpdate TAccessApprovalPoliciesBypassersUpdate
>; >;
[TableName.AccessApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
TAccessApprovalPoliciesEnvironments,
TAccessApprovalPoliciesEnvironmentsInsert,
TAccessApprovalPoliciesEnvironmentsUpdate
>;
[TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType< [TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType<
TAccessApprovalRequests, TAccessApprovalRequests,
TAccessApprovalRequestsInsert, TAccessApprovalRequestsInsert,
@@ -935,6 +951,11 @@ declare module "knex/types/tables" {
TSecretApprovalRequestSecretTagsInsert, TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate TSecretApprovalRequestSecretTagsUpdate
>; >;
[TableName.SecretApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
TSecretApprovalPoliciesEnvironments,
TSecretApprovalPoliciesEnvironmentsInsert,
TSecretApprovalPoliciesEnvironmentsUpdate
>;
[TableName.SecretRotation]: KnexOriginal.CompositeTableType< [TableName.SecretRotation]: KnexOriginal.CompositeTableType<
TSecretRotations, TSecretRotations,
TSecretRotationsInsert, TSecretRotationsInsert,
@@ -0,0 +1,96 @@
import { Knex } from "knex";
import { selectAllTableCols } from "@app/lib/knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment))) {
await knex.schema.createTable(TableName.AccessApprovalPolicyEnvironment, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("policyId").notNullable();
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
t.uuid("envId").notNullable();
t.foreign("envId").references("id").inTable(TableName.Environment);
t.timestamps(true, true, true);
t.unique(["policyId", "envId"]);
});
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment);
const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy)
.select(selectAllTableCols(TableName.AccessApprovalPolicy))
.whereNotNull(`${TableName.AccessApprovalPolicy}.envId`);
const accessApprovalPolicies = existingAccessApprovalPolicies.map(async (policy) => {
await knex(TableName.AccessApprovalPolicyEnvironment).insert({
policyId: policy.id,
envId: policy.envId
});
});
await Promise.all(accessApprovalPolicies);
}
if (!(await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment))) {
await knex.schema.createTable(TableName.SecretApprovalPolicyEnvironment, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("policyId").notNullable();
t.foreign("policyId").references("id").inTable(TableName.SecretApprovalPolicy).onDelete("CASCADE");
t.uuid("envId").notNullable();
t.foreign("envId").references("id").inTable(TableName.Environment);
t.timestamps(true, true, true);
t.unique(["policyId", "envId"]);
});
await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment);
const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy)
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
.whereNotNull(`${TableName.SecretApprovalPolicy}.envId`);
const secretApprovalPolicies = existingSecretApprovalPolicies.map(async (policy) => {
await knex(TableName.SecretApprovalPolicyEnvironment).insert({
policyId: policy.id,
envId: policy.envId
});
});
await Promise.all(secretApprovalPolicies);
}
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
// Add the new foreign key constraint with ON DELETE SET NULL
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
});
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
// Add the new foreign key constraint with ON DELETE SET NULL
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
});
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment)) {
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyEnvironment);
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment);
}
if (await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment)) {
await knex.schema.dropTableIfExists(TableName.SecretApprovalPolicyEnvironment);
await dropOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment);
}
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
});
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
});
}
@@ -0,0 +1,111 @@
/* eslint-disable no-await-in-loop */
import { Knex } from "knex";
import { chunkArray } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TableName } from "../schemas";
import { TReminders, TRemindersInsert } from "../schemas/reminders";
export async function up(knex: Knex): Promise<void> {
logger.info("Initializing secret reminders migration");
const hasReminderTable = await knex.schema.hasTable(TableName.Reminder);
if (hasReminderTable) {
const secretsWithLatestVersions = await knex(TableName.SecretV2)
.whereNotNull(`${TableName.SecretV2}.reminderRepeatDays`)
.whereRaw(`"${TableName.SecretV2}"."reminderRepeatDays" > 0`)
.innerJoin(TableName.SecretVersionV2, (qb) => {
void qb
.on(`${TableName.SecretVersionV2}.secretId`, "=", `${TableName.SecretV2}.id`)
.andOn(`${TableName.SecretVersionV2}.reminderRepeatDays`, "=", `${TableName.SecretV2}.reminderRepeatDays`);
})
.whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => {
void qb
.select(["v2.secretId", knex.raw("MIN(v2.version) as version")])
.from(`${TableName.SecretVersionV2} as v2`)
.innerJoin(`${TableName.SecretV2} as s2`, "v2.secretId", "s2.id")
.whereRaw(`v2."reminderRepeatDays" = s2."reminderRepeatDays"`)
.whereNotNull("v2.reminderRepeatDays")
.whereRaw(`v2."reminderRepeatDays" > 0`)
.groupBy("v2.secretId");
})
// Add LEFT JOIN with Reminder table to check for existing reminders
.leftJoin(TableName.Reminder, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`)
// Only include secrets that don't already have reminders
.whereNull(`${TableName.Reminder}.secretId`)
.select(
knex.ref("id").withSchema(TableName.SecretV2).as("secretId"),
knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"),
knex.ref("reminderNote").withSchema(TableName.SecretV2).as("reminderNote"),
knex.ref("createdAt").withSchema(TableName.SecretVersionV2).as("createdAt")
);
logger.info(`Found ${secretsWithLatestVersions.length} reminders to migrate`);
const reminderInserts: TRemindersInsert[] = [];
if (secretsWithLatestVersions.length > 0) {
secretsWithLatestVersions.forEach((secret) => {
if (!secret.reminderRepeatDays) return;
const now = new Date();
const createdAt = new Date(secret.createdAt);
let nextReminderDate = new Date(createdAt);
nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays);
// If the next reminder date is in the past, calculate the proper next occurrence
if (nextReminderDate < now) {
const daysSinceCreation = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24));
const daysIntoCurrentCycle = daysSinceCreation % secret.reminderRepeatDays;
const daysUntilNextReminder = secret.reminderRepeatDays - daysIntoCurrentCycle;
nextReminderDate = new Date(now);
nextReminderDate.setDate(now.getDate() + daysUntilNextReminder);
}
reminderInserts.push({
secretId: secret.secretId,
message: secret.reminderNote,
repeatDays: secret.reminderRepeatDays,
nextReminderDate
});
});
const commitBatches = chunkArray(reminderInserts, 2000);
for (const commitBatch of commitBatches) {
const insertedReminders = (await knex
.batchInsert(TableName.Reminder, commitBatch)
.returning("*")) as TReminders[];
const insertedReminderSecretIds = insertedReminders.map((reminder) => reminder.secretId).filter(Boolean);
const recipients = await knex(TableName.SecretReminderRecipients)
.whereRaw(`??.?? IN (${insertedReminderSecretIds.map(() => "?").join(",")})`, [
TableName.SecretReminderRecipients,
"secretId",
...insertedReminderSecretIds
])
.select(
knex.ref("userId").withSchema(TableName.SecretReminderRecipients).as("userId"),
knex.ref("secretId").withSchema(TableName.SecretReminderRecipients).as("secretId")
);
const reminderRecipients = recipients.map((recipient) => ({
reminderId: insertedReminders.find((reminder) => reminder.secretId === recipient.secretId)?.id,
userId: recipient.userId
}));
const filteredRecipients = reminderRecipients.filter((recipient) => Boolean(recipient.reminderId));
await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients);
}
logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`);
}
logger.info("Secret reminders migration completed");
} else {
logger.warn("Reminder table does not exist, skipping migration");
}
}
export async function down(): Promise<void> {
logger.info("Rollback not implemented for secret reminders fix migration");
}
@@ -53,7 +53,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory
let envCfg = Object.freeze(parsedEnv.data); let envCfg = Object.freeze(parsedEnv.data);
const fipsEnabled = await crypto.initialize(superAdminDAL); const fipsEnabled = await crypto.initialize(superAdminDAL, envCfg);
// Fix for 128-bit entropy encryption key expansion issue: // Fix for 128-bit entropy encryption key expansion issue:
// In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY. // In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY.
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesEnvironmentsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAccessApprovalPoliciesEnvironments = z.infer<typeof AccessApprovalPoliciesEnvironmentsSchema>;
export type TAccessApprovalPoliciesEnvironmentsInsert = Omit<
z.input<typeof AccessApprovalPoliciesEnvironmentsSchema>,
TImmutableDBKeys
>;
export type TAccessApprovalPoliciesEnvironmentsUpdate = Partial<
Omit<z.input<typeof AccessApprovalPoliciesEnvironmentsSchema>, TImmutableDBKeys>
>;
+2
View File
@@ -100,6 +100,7 @@ export enum TableName {
AccessApprovalPolicyBypasser = "access_approval_policies_bypassers", AccessApprovalPolicyBypasser = "access_approval_policies_bypassers",
AccessApprovalRequest = "access_approval_requests", AccessApprovalRequest = "access_approval_requests",
AccessApprovalRequestReviewer = "access_approval_requests_reviewers", AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
AccessApprovalPolicyEnvironment = "access_approval_policies_environments",
SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers", SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers",
@@ -107,6 +108,7 @@ export enum TableName {
SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestReviewer = "secret_approval_requests_reviewers",
SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecret = "secret_approval_requests_secrets",
SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags", SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags",
SecretApprovalPolicyEnvironment = "secret_approval_policies_environments",
SecretRotation = "secret_rotations", SecretRotation = "secret_rotations",
SecretRotationOutput = "secret_rotation_outputs", SecretRotationOutput = "secret_rotation_outputs",
SamlConfig = "saml_configs", SamlConfig = "saml_configs",
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesEnvironmentsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretApprovalPoliciesEnvironments = z.infer<typeof SecretApprovalPoliciesEnvironmentsSchema>;
export type TSecretApprovalPoliciesEnvironmentsInsert = Omit<
z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>,
TImmutableDBKeys
>;
export type TSecretApprovalPoliciesEnvironmentsUpdate = Partial<
Omit<z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>, TImmutableDBKeys>
>;
@@ -17,52 +17,66 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z
projectSlug: z.string().trim(), .object({
name: z.string().optional(), projectSlug: z.string().trim(),
secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash), name: z.string().optional(),
environment: z.string(), secretPath: z
approvers: z .string()
.discriminatedUnion("type", [ .trim()
z.object({ .min(1, { message: "Secret path cannot be empty" })
type: z.literal(ApproverType.Group), .transform(removeTrailingSlash),
id: z.string(), environment: z.string().optional(),
sequence: z.number().int().default(1) environments: z.string().array().optional(),
}), approvers: z
z.object({ .discriminatedUnion("type", [
type: z.literal(ApproverType.User), z.object({
id: z.string().optional(), type: z.literal(ApproverType.Group),
username: z.string().optional(), id: z.string(),
sequence: z.number().int().default(1) sequence: z.number().int().default(1)
}),
z.object({
type: z.literal(ApproverType.User),
id: z.string().optional(),
username: z.string().optional(),
sequence: z.number().int().default(1)
})
])
.array()
.max(100, "Cannot have more than 100 approvers")
.min(1, { message: "At least one approver should be provided" })
.refine(
// @ts-expect-error this is ok
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
"Must provide either username or id"
),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({
type: z.literal(BypasserType.User),
id: z.string().optional(),
username: z.string().optional()
})
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
}) })
]) .array()
.array() .optional(),
.max(100, "Cannot have more than 100 approvers") approvals: z.number().min(1).default(1),
.min(1, { message: "At least one approver should be provided" }) enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
.refine( allowedSelfApprovals: z.boolean().default(true)
// @ts-expect-error this is ok })
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)), .refine(
"Must provide either username or id" (val) => Boolean(val.environment) || Boolean(val.environments),
), "Must provide either environment or environments"
bypassers: z ),
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
})
.array()
.optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true)
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -78,7 +92,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectSlug: req.body.projectSlug, projectSlug: req.body.projectSlug,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, name:
req.body.name ?? `${req.body.environment || req.body.environments?.join("-").substring(0, 250)}-${nanoid(3)}`,
enforcementLevel: req.body.enforcementLevel enforcementLevel: req.body.enforcementLevel
}); });
return { approval }; return { approval };
@@ -211,6 +226,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: z.number().min(1).optional(), approvals: z.number().min(1).optional(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true), allowedSelfApprovals: z.boolean().default(true),
environments: z.array(z.string()).optional(),
approvalsRequired: z approvalsRequired: z
.object({ .object({
numberOfApprovals: z.number().int(), numberOfApprovals: z.number().int(),
@@ -17,34 +17,45 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z
workspaceId: z.string(), .object({
name: z.string().optional(), workspaceId: z.string(),
environment: z.string(), name: z.string().optional(),
secretPath: z environment: z.string().optional(),
.string() environments: z.string().array().optional(),
.min(1, { message: "Secret path cannot be empty" }) secretPath: z
.transform((val) => removeTrailingSlash(val)), .string()
approvers: z .min(1, { message: "Secret path cannot be empty" })
.discriminatedUnion("type", [ .transform((val) => removeTrailingSlash(val)),
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), approvers: z
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() }) .discriminatedUnion("type", [
]) z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
.array() z.object({
.min(1, { message: "At least one approver should be provided" }) type: z.literal(ApproverType.User),
.max(100, "Cannot have more than 100 approvers"), id: z.string().optional(),
bypassers: z username: z.string().optional()
.discriminatedUnion("type", [ })
z.object({ type: z.literal(BypasserType.Group), id: z.string() }), ])
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() }) .array()
]) .min(1, { message: "At least one approver should be provided" })
.array() .max(100, "Cannot have more than 100 approvers"),
.max(100, "Cannot have more than 100 bypassers") bypassers: z
.optional(), .discriminatedUnion("type", [
approvals: z.number().min(1).default(1), z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), z.object({
allowedSelfApprovals: z.boolean().default(true) type: z.literal(BypasserType.User),
}), id: z.string().optional(),
username: z.string().optional()
})
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true)
})
.refine((data) => data.environment || data.environments, "At least one environment should be provided"),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -60,7 +71,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
...req.body, ...req.body,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`,
enforcementLevel: req.body.enforcementLevel enforcementLevel: req.body.enforcementLevel
}); });
return { approval }; return { approval };
@@ -103,7 +114,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.optional() .optional()
.transform((val) => (val ? removeTrailingSlash(val) : undefined)), .transform((val) => (val ? removeTrailingSlash(val) : undefined)),
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true),
environments: z.array(z.string()).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -26,6 +26,7 @@ export interface TAccessApprovalPolicyDALFactory
>, >,
customFilter?: { customFilter?: {
policyId?: string; policyId?: string;
envId?: string;
}, },
tx?: Knex tx?: Knex
) => Promise< ) => Promise<
@@ -55,11 +56,6 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath: string; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string; projectId: string;
bypassers: ( bypassers: (
| { | {
@@ -72,6 +68,11 @@ export interface TAccessApprovalPolicyDALFactory
type: BypasserType.Group; type: BypasserType.Group;
} }
)[]; )[];
environments: {
id: string;
name: string;
slug: string;
}[];
}[] }[]
>; >;
findById: ( findById: (
@@ -95,11 +96,11 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath: string; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environments: {
id: string; id: string;
name: string; name: string;
slug: string; slug: string;
}; }[];
projectId: string; projectId: string;
} }
| undefined | undefined
@@ -143,6 +144,26 @@ export interface TAccessApprovalPolicyDALFactory
} }
| undefined | undefined
>; >;
findPolicyByEnvIdAndSecretPath: (
{ envIds, secretPath }: { envIds: string[]; secretPath: string },
tx?: Knex
) => Promise<{
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string;
}>;
} }
export interface TAccessApprovalPolicyServiceFactory { export interface TAccessApprovalPolicyServiceFactory {
@@ -367,6 +388,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
policyId?: string; policyId?: string;
envId?: string;
} }
) => { ) => {
const result = await tx(TableName.AccessApprovalPolicy) const result = await tx(TableName.AccessApprovalPolicy)
@@ -377,7 +399,17 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId); void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
} }
}) })
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .join(
TableName.AccessApprovalPolicyEnvironment,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`
)
.join(TableName.Environment, `${TableName.AccessApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`)
.where((qb) => {
if (customFilter?.envId) {
void qb.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", customFilter.envId);
}
})
.leftJoin( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
@@ -404,7 +436,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser)) .select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName")) .select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId")) .select(tx.ref("id").withSchema(TableName.Environment).as("environmentId"))
.select(tx.ref("projectId").withSchema(TableName.Environment)) .select(tx.ref("projectId").withSchema(TableName.Environment))
.select(selectAllTableCols(TableName.AccessApprovalPolicy)); .select(selectAllTableCols(TableName.AccessApprovalPolicy));
@@ -448,6 +480,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
sequence: approverSequence, sequence: approverSequence,
approvalsRequired approvalsRequired
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId: id, envName, envSlug }) => ({
id,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -470,11 +511,6 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
data: docs, data: docs,
key: "id", key: "id",
parentMapper: (data) => ({ parentMapper: (data) => ({
environment: {
id: data.envId,
name: data.envName,
slug: data.envSlug
},
projectId: data.projectId, projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data) ...AccessApprovalPoliciesSchema.parse(data)
// secretPath: data.secretPath || undefined, // secretPath: data.secretPath || undefined,
@@ -517,6 +553,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
id, id,
type: BypasserType.Group as const type: BypasserType.Group as const
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId: id, envName, envSlug }) => ({
id,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -545,14 +590,20 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
buildFindFilter( buildFindFilter(
{ {
envId,
secretPath secretPath
}, },
TableName.AccessApprovalPolicy TableName.AccessApprovalPolicy
) )
) )
.join(
TableName.AccessApprovalPolicyEnvironment,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", envId)
.orderBy("deletedAt", "desc") .orderBy("deletedAt", "desc")
.orderByRaw(`"deletedAt" IS NULL`) .orderByRaw(`"deletedAt" IS NULL`)
.select(selectAllTableCols(TableName.AccessApprovalPolicy))
.first(); .first();
return result; return result;
@@ -561,5 +612,81 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
} }
}; };
return { ...accessApprovalPolicyOrm, find, findById, softDeleteById, findLastValidPolicy }; const findPolicyByEnvIdAndSecretPath: TAccessApprovalPolicyDALFactory["findPolicyByEnvIdAndSecretPath"] = async (
{ envIds, secretPath },
tx
) => {
try {
const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicy)
.join(
TableName.AccessApprovalPolicyEnvironment,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
.join(
TableName.Environment,
`${TableName.AccessApprovalPolicyEnvironment}.envId`,
`${TableName.Environment}.id`
)
.where(
// eslint-disable-next-line @typescript-eslint/no-misused-promises
buildFindFilter(
{
$in: {
envId: envIds
}
},
TableName.AccessApprovalPolicyEnvironment
)
)
.where(
// eslint-disable-next-line @typescript-eslint/no-misused-promises
buildFindFilter(
{
secretPath
},
TableName.AccessApprovalPolicy
)
)
.whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`)
.orderBy("deletedAt", "desc")
.orderByRaw(`"deletedAt" IS NULL`)
.select(selectAllTableCols(TableName.AccessApprovalPolicy))
.select(db.ref("name").withSchema(TableName.Environment).as("envName"))
.select(db.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(db.ref("id").withSchema(TableName.Environment).as("environmentId"))
.select(db.ref("projectId").withSchema(TableName.Environment));
const formattedDocs = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data)
}),
childrenMapper: [
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId: id, envName, envSlug }) => ({
id,
name: envName,
slug: envSlug
})
}
]
});
return formattedDocs?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "findPolicyByEnvIdAndSecretPath" });
}
};
return {
...accessApprovalPolicyOrm,
find,
findById,
softDeleteById,
findLastValidPolicy,
findPolicyByEnvIdAndSecretPath
};
}; };
@@ -0,0 +1,32 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
export type TAccessApprovalPolicyEnvironmentDALFactory = ReturnType<typeof accessApprovalPolicyEnvironmentDALFactory>;
export const accessApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
const accessApprovalPolicyEnvironmentOrm = ormify(db, TableName.AccessApprovalPolicyEnvironment);
const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicyEnvironment)
.join(
TableName.AccessApprovalPolicy,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ envId }, TableName.AccessApprovalPolicyEnvironment))
.whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`)
.select(selectAllTableCols(TableName.AccessApprovalPolicyEnvironment));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" });
}
};
return { ...accessApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId };
};
@@ -21,6 +21,7 @@ import {
TAccessApprovalPolicyBypasserDALFactory TAccessApprovalPolicyBypasserDALFactory
} from "./access-approval-policy-approver-dal"; } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { TAccessApprovalPolicyEnvironmentDALFactory } from "./access-approval-policy-environment-dal";
import { import {
ApproverType, ApproverType,
BypasserType, BypasserType,
@@ -45,12 +46,14 @@ type TAccessApprovalPolicyServiceFactoryDep = {
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">; accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
accessApprovalPolicyEnvironmentDAL: TAccessApprovalPolicyEnvironmentDALFactory;
}; };
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL, accessApprovalPolicyBypasserDAL,
accessApprovalPolicyEnvironmentDAL,
groupDAL, groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
@@ -63,21 +66,22 @@ export const accessApprovalPolicyServiceFactory = ({
}: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => {
const $policyExists = async ({ const $policyExists = async ({
envId, envId,
envIds,
secretPath, secretPath,
policyId policyId
}: { }: {
envId: string; envId?: string;
envIds?: string[];
secretPath: string; secretPath: string;
policyId?: string; policyId?: string;
}) => { }) => {
const policy = await accessApprovalPolicyDAL if (!envId && !envIds) {
.findOne({ throw new BadRequestError({ message: "Must provide either envId or envIds" });
envId, }
secretPath, const policy = await accessApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({
deletedAt: null secretPath,
}) envIds: envId ? [envId] : (envIds as string[])
.catch(() => null); });
return policyId ? policy && policy.id !== policyId : Boolean(policy); return policyId ? policy && policy.id !== policyId : Boolean(policy);
}; };
@@ -93,6 +97,7 @@ export const accessApprovalPolicyServiceFactory = ({
bypassers, bypassers,
projectSlug, projectSlug,
environment, environment,
environments,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired
@@ -125,13 +130,23 @@ export const accessApprovalPolicyServiceFactory = ({
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const mergedEnvs = (environment ? [environment] : environments) || [];
if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); if (mergedEnvs.length === 0) {
throw new BadRequestError({ message: "Must provide either environment or environments" });
}
const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId: project.id });
if (!envs.length || envs.length !== mergedEnvs.length) {
const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env));
throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` });
}
if (await $policyExists({ envId: env.id, secretPath })) { for (const env of envs) {
throw new BadRequestError({ // eslint-disable-next-line no-await-in-loop
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` if (await $policyExists({ envId: env.id, secretPath })) {
}); throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'`
});
}
} }
let approverUserIds = userApprovers; let approverUserIds = userApprovers;
@@ -199,7 +214,7 @@ export const accessApprovalPolicyServiceFactory = ({
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create( const doc = await accessApprovalPolicyDAL.create(
{ {
envId: env.id, envId: envs[0].id,
approvals, approvals,
secretPath, secretPath,
name, name,
@@ -208,6 +223,10 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await accessApprovalPolicyEnvironmentDAL.insertMany(
envs.map((el) => ({ policyId: doc.id, envId: el.id })),
tx
);
if (approverUserIds.length) { if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
@@ -260,7 +279,7 @@ export const accessApprovalPolicyServiceFactory = ({
return doc; return doc;
}); });
return { ...accessApproval, environment: env, projectId: project.id }; return { ...accessApproval, environments: envs, projectId: project.id, environment: envs[0] };
}; };
const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] = const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] =
@@ -279,7 +298,10 @@ export const accessApprovalPolicyServiceFactory = ({
}); });
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
return accessApprovalPolicies; return accessApprovalPolicies.map((policy) => ({
...policy,
environment: policy.environments[0]
}));
}; };
const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({ const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({
@@ -295,7 +317,8 @@ export const accessApprovalPolicyServiceFactory = ({
approvals, approvals,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}: TUpdateAccessApprovalPolicy) => { }: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group); const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
@@ -323,16 +346,27 @@ export const accessApprovalPolicyServiceFactory = ({
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
} }
let envs = accessApprovalPolicy.environments;
if ( if (
await $policyExists({ environments &&
envId: accessApprovalPolicy.envId, (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
secretPath: secretPath || accessApprovalPolicy.secretPath,
policyId: accessApprovalPolicy.id
})
) { ) {
throw new BadRequestError({ envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: accessApprovalPolicy.projectId });
message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'` }
});
for (const env of envs) {
if (
// eslint-disable-next-line no-await-in-loop
await $policyExists({
envId: env.id,
secretPath: secretPath || accessApprovalPolicy.secretPath,
policyId: accessApprovalPolicy.id
})
) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath || accessApprovalPolicy.secretPath}' already exists in environment '${env.slug}'`
});
}
} }
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@@ -488,6 +522,14 @@ export const accessApprovalPolicyServiceFactory = ({
); );
} }
if (environments) {
await accessApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyEnvironmentDAL.insertMany(
envs.map((env) => ({ policyId: doc.id, envId: env.id })),
tx
);
}
await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx); await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) { if (bypasserUserIds.length) {
@@ -517,7 +559,8 @@ export const accessApprovalPolicyServiceFactory = ({
return { return {
...updatedPolicy, ...updatedPolicy,
environment: accessApprovalPolicy.environment, environments: accessApprovalPolicy.environments,
environment: accessApprovalPolicy.environments[0],
projectId: accessApprovalPolicy.projectId projectId: accessApprovalPolicy.projectId
}; };
}; };
@@ -568,7 +611,10 @@ export const accessApprovalPolicyServiceFactory = ({
} }
}); });
return policy; return {
...policy,
environment: policy.environments[0]
};
}; };
const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({ const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({
@@ -598,11 +644,13 @@ export const accessApprovalPolicyServiceFactory = ({
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` }); if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
const policies = await accessApprovalPolicyDAL.find({ const policies = await accessApprovalPolicyDAL.find(
envId: environment.id, {
projectId: project.id, projectId: project.id,
deletedAt: null deletedAt: null
}); },
{ envId: environment.id }
);
if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` }); if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` });
return { count: policies.length }; return { count: policies.length };
@@ -634,7 +682,10 @@ export const accessApprovalPolicyServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return policy; return {
...policy,
environment: policy.environments[0]
};
}; };
return { return {
@@ -26,7 +26,8 @@ export enum BypasserType {
export type TCreateAccessApprovalPolicy = { export type TCreateAccessApprovalPolicy = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment?: string;
environments?: string[];
approvers: ( approvers: (
| { type: ApproverType.Group; id: string; sequence?: number } | { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number } | { type: ApproverType.User; id?: string; username?: string; sequence?: number }
@@ -58,6 +59,7 @@ export type TUpdateAccessApprovalPolicy = {
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
environments?: string[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteAccessApprovalPolicy = { export type TDeleteAccessApprovalPolicy = {
@@ -113,6 +115,15 @@ export interface TAccessApprovalPolicyServiceFactory {
slug: string; slug: string;
position: number; position: number;
}; };
environments: {
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
projectId: string;
slug: string;
position: number;
}[];
projectId: string; projectId: string;
name: string; name: string;
id: string; id: string;
@@ -153,6 +164,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
}>; }>;
updateAccessApprovalPolicy: ({ updateAccessApprovalPolicy: ({
@@ -168,13 +184,19 @@ export interface TAccessApprovalPolicyServiceFactory {
approvals, approvals,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}: TUpdateAccessApprovalPolicy) => Promise<{ }: TUpdateAccessApprovalPolicy) => Promise<{
environment: { environment: {
id: string; id: string;
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
name: string; name: string;
id: string; id: string;
@@ -225,6 +247,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
bypassers: ( bypassers: (
| { | {
@@ -276,6 +303,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
bypassers: ( bypassers: (
| { | {
@@ -65,7 +65,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
deletedAt: Date | null | undefined; deletedAt: Date | null | undefined;
}; };
projectId: string; projectId: string;
environment: string; environments: string[];
requestedByUser: { requestedByUser: {
userId: string; userId: string;
email: string | null | undefined; email: string | null | undefined;
@@ -515,7 +515,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
`accessApprovalReviewerUser.id` `accessApprovalReviewerUser.id`
) )
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .leftJoin(
TableName.AccessApprovalPolicyEnvironment,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`
)
.leftJoin(
TableName.Environment,
`${TableName.AccessApprovalPolicyEnvironment}.envId`,
`${TableName.Environment}.id`
)
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
@@ -683,6 +693,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
lastName, lastName,
username username
}) })
},
{
key: "environment",
label: "environments" as const,
mapper: ({ environment }) => environment
} }
] ]
}); });
@@ -86,6 +86,25 @@ export const accessApprovalRequestServiceFactory = ({
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
projectSlackConfigDAL projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => { }: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => {
const $getEnvironmentFromPermissions = (permissions: unknown): string | null => {
if (!Array.isArray(permissions) || permissions.length === 0) {
return null;
}
const firstPermission = permissions[0] as unknown[];
if (!Array.isArray(firstPermission) || firstPermission.length < 3) {
return null;
}
const metadata = firstPermission[2] as Record<string, unknown>;
if (typeof metadata === "object" && metadata !== null && "environment" in metadata) {
const env = metadata.environment;
return typeof env === "string" ? env : null;
}
return null;
};
const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({ const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({
isTemporary, isTemporary,
temporaryRange, temporaryRange,
@@ -308,6 +327,15 @@ export const accessApprovalRequestServiceFactory = ({
requests = requests.filter((request) => request.environment === envSlug); requests = requests.filter((request) => request.environment === envSlug);
} }
requests = requests.map((request) => {
const permissionEnvironment = $getEnvironmentFromPermissions(request.permissions);
if (permissionEnvironment) {
request.environmentName = permissionEnvironment;
}
return request;
});
return { requests }; return { requests };
}; };
@@ -325,13 +353,27 @@ export const accessApprovalRequestServiceFactory = ({
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` }); throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
} }
const { policy, environment } = accessApprovalRequest; const { policy, environments, permissions } = accessApprovalRequest;
if (policy.deletedAt) { if (policy.deletedAt) {
throw new BadRequestError({ throw new BadRequestError({
message: "The policy associated with this access request has been deleted." message: "The policy associated with this access request has been deleted."
}); });
} }
const permissionEnvironment = $getEnvironmentFromPermissions(permissions);
if (
!permissionEnvironment ||
(!environments.includes(permissionEnvironment) && status === ApprovalStatus.APPROVED)
) {
throw new BadRequestError({
message: `The original policy ${policy.name} is not attached to environment '${permissionEnvironment}'.`
});
}
const environment = await projectEnvDAL.findOne({
projectId: accessApprovalRequest.projectId,
slug: permissionEnvironment
});
const { membership, hasRole } = await permissionService.getProjectPermission({ const { membership, hasRole } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -553,7 +595,7 @@ export const accessApprovalRequestServiceFactory = ({
requesterEmail: actingUser.email, requesterEmail: actingUser.email,
bypassReason: bypassReason || "No reason provided", bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/", secretPath: policy.secretPath || "/",
environment, environment: environment?.name || permissionEnvironment,
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`, approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`,
requestType: "access" requestType: "access"
}, },
@@ -23,6 +23,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
sapId?: string; sapId?: string;
envId?: string;
} }
) => ) =>
tx(TableName.SecretApprovalPolicy) tx(TableName.SecretApprovalPolicy)
@@ -33,7 +34,17 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId); void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
} }
}) })
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`) .join(
TableName.SecretApprovalPolicyEnvironment,
`${TableName.SecretApprovalPolicyEnvironment}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.join(TableName.Environment, `${TableName.SecretApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`)
.where((qb) => {
if (customFilter?.envId) {
void qb.where(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", customFilter.envId);
}
})
.leftJoin( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
@@ -97,7 +108,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
.select( .select(
tx.ref("name").withSchema(TableName.Environment).as("envName"), tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"), tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
tx.ref("id").withSchema(TableName.Environment).as("envId"), tx.ref("id").withSchema(TableName.Environment).as("environmentId"),
tx.ref("projectId").withSchema(TableName.Environment) tx.ref("projectId").withSchema(TableName.Environment)
) )
.select(selectAllTableCols(TableName.SecretApprovalPolicy)) .select(selectAllTableCols(TableName.SecretApprovalPolicy))
@@ -146,6 +157,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
firstName, firstName,
lastName lastName
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId, envName, envSlug }) => ({
id: environmentId,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -160,6 +180,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
sapId?: string; sapId?: string;
envId?: string;
}, },
tx?: Knex tx?: Knex
) => { ) => {
@@ -221,6 +242,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
mapper: ({ approverGroupUserId: userId }) => ({ mapper: ({ approverGroupUserId: userId }) => ({
userId userId
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId, envName, envSlug }) => ({
id: environmentId,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -235,5 +265,74 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
return softDeletedPolicy; return softDeletedPolicy;
}; };
return { ...secretApprovalPolicyOrm, findById, find, softDeleteById }; const findPolicyByEnvIdAndSecretPath = async (
{ envIds, secretPath }: { envIds: string[]; secretPath: string },
tx?: Knex
) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicy)
.join(
TableName.SecretApprovalPolicyEnvironment,
`${TableName.SecretApprovalPolicyEnvironment}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.join(
TableName.Environment,
`${TableName.SecretApprovalPolicyEnvironment}.envId`,
`${TableName.Environment}.id`
)
.where(
// eslint-disable-next-line @typescript-eslint/no-misused-promises
buildFindFilter(
{
$in: {
envId: envIds
}
},
TableName.SecretApprovalPolicyEnvironment
)
)
.where(
// eslint-disable-next-line @typescript-eslint/no-misused-promises
buildFindFilter(
{
secretPath
},
TableName.SecretApprovalPolicy
)
)
.whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`)
.orderBy("deletedAt", "desc")
.orderByRaw(`"deletedAt" IS NULL`)
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
.select(db.ref("name").withSchema(TableName.Environment).as("envName"))
.select(db.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(db.ref("id").withSchema(TableName.Environment).as("environmentId"))
.select(db.ref("projectId").withSchema(TableName.Environment));
const formattedDocs = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
projectId: data.projectId,
...SecretApprovalPoliciesSchema.parse(data)
}),
childrenMapper: [
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId: id, envName, envSlug }) => ({
id,
name: envName,
slug: envSlug
})
}
]
});
return formattedDocs?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "findPolicyByEnvIdAndSecretPath" });
}
};
return { ...secretApprovalPolicyOrm, findById, find, softDeleteById, findPolicyByEnvIdAndSecretPath };
}; };
@@ -0,0 +1,32 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretApprovalPolicyEnvironmentDALFactory = ReturnType<typeof secretApprovalPolicyEnvironmentDALFactory>;
export const secretApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
const secretApprovalPolicyEnvironmentOrm = ormify(db, TableName.SecretApprovalPolicyEnvironment);
const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicyEnvironment)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicyEnvironment}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ envId }, TableName.SecretApprovalPolicyEnvironment))
.whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`)
.select(selectAllTableCols(TableName.SecretApprovalPolicyEnvironment));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" });
}
};
return { ...secretApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId };
};
@@ -19,6 +19,7 @@ import {
TSecretApprovalPolicyBypasserDALFactory TSecretApprovalPolicyBypasserDALFactory
} from "./secret-approval-policy-approver-dal"; } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import { TSecretApprovalPolicyEnvironmentDALFactory } from "./secret-approval-policy-environment-dal";
import { import {
TCreateSapDTO, TCreateSapDTO,
TDeleteSapDTO, TDeleteSapDTO,
@@ -36,12 +37,13 @@ const getPolicyScore = (policy: { secretPath?: string | null }) =>
type TSecretApprovalPolicyServiceFactoryDep = { type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
userDAL: Pick<TUserDALFactory, "find">; userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory; secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">; secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">;
secretApprovalPolicyEnvironmentDAL: TSecretApprovalPolicyEnvironmentDALFactory;
}; };
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>; export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@@ -51,27 +53,30 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService, permissionService,
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
secretApprovalPolicyBypasserDAL, secretApprovalPolicyBypasserDAL,
secretApprovalPolicyEnvironmentDAL,
projectEnvDAL, projectEnvDAL,
userDAL, userDAL,
licenseService, licenseService,
secretApprovalRequestDAL secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const $policyExists = async ({ const $policyExists = async ({
envIds,
envId, envId,
secretPath, secretPath,
policyId policyId
}: { }: {
envId: string; envIds?: string[];
envId?: string;
secretPath: string; secretPath: string;
policyId?: string; policyId?: string;
}) => { }) => {
const policy = await secretApprovalPolicyDAL if (!envIds && !envId) {
.findOne({ throw new BadRequestError({ message: "At least one environment should be provided" });
envId, }
secretPath, const policy = await secretApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({
deletedAt: null envIds: envId ? [envId] : envIds || [],
}) secretPath
.catch(() => null); });
return policyId ? policy && policy.id !== policyId : Boolean(policy); return policyId ? policy && policy.id !== policyId : Boolean(policy);
}; };
@@ -88,6 +93,7 @@ export const secretApprovalPolicyServiceFactory = ({
projectId, projectId,
secretPath, secretPath,
environment, environment,
environments,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals
}: TCreateSapDTO) => { }: TCreateSapDTO) => {
@@ -127,17 +133,23 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
const env = await projectEnvDAL.findOne({ slug: environment, projectId }); const mergedEnvs = (environment ? [environment] : environments) || [];
if (!env) { if (mergedEnvs.length === 0) {
throw new NotFoundError({ throw new BadRequestError({ message: "Must provide either environment or environments" });
message: `Environment with slug '${environment}' not found in project with ID ${projectId}` }
}); const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId });
if (!envs.length || envs.length !== mergedEnvs.length) {
const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env));
throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` });
} }
if (await $policyExists({ envId: env.id, secretPath })) { for (const env of envs) {
throw new BadRequestError({ // eslint-disable-next-line no-await-in-loop
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` if (await $policyExists({ envId: env.id, secretPath })) {
}); throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'`
});
}
} }
let groupBypassers: string[] = []; let groupBypassers: string[] = [];
@@ -181,7 +193,7 @@ export const secretApprovalPolicyServiceFactory = ({
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => { const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.create( const doc = await secretApprovalPolicyDAL.create(
{ {
envId: env.id, envId: envs[0].id,
approvals, approvals,
secretPath, secretPath,
name, name,
@@ -190,6 +202,13 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await secretApprovalPolicyEnvironmentDAL.insertMany(
envs.map((env) => ({
envId: env.id,
policyId: doc.id
})),
tx
);
let userApproverIds = userApprovers; let userApproverIds = userApprovers;
if (userApproverNames.length) { if (userApproverNames.length) {
@@ -253,12 +272,13 @@ export const secretApprovalPolicyServiceFactory = ({
return doc; return doc;
}); });
return { ...secretApproval, environment: env, projectId }; return { ...secretApproval, environments: envs, projectId, environment: envs[0] };
}; };
const updateSecretApprovalPolicy = async ({ const updateSecretApprovalPolicy = async ({
approvers, approvers,
bypassers, bypassers,
environments,
secretPath, secretPath,
name, name,
actorId, actorId,
@@ -288,17 +308,26 @@ export const secretApprovalPolicyServiceFactory = ({
message: `Secret approval policy with ID '${secretPolicyId}' not found` message: `Secret approval policy with ID '${secretPolicyId}' not found`
}); });
} }
let envs = secretApprovalPolicy.environments;
if ( if (
await $policyExists({ environments &&
envId: secretApprovalPolicy.envId, (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
secretPath: secretPath || secretApprovalPolicy.secretPath,
policyId: secretApprovalPolicy.id
})
) { ) {
throw new BadRequestError({ envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: secretApprovalPolicy.projectId });
message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'` }
}); for (const env of envs) {
if (
// eslint-disable-next-line no-await-in-loop
await $policyExists({
envId: env.id,
secretPath: secretPath || secretApprovalPolicy.secretPath,
policyId: secretApprovalPolicy.id
})
) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath || secretApprovalPolicy.secretPath}' already exists in environment '${env.slug}'`
});
}
} }
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@@ -415,6 +444,17 @@ export const secretApprovalPolicyServiceFactory = ({
); );
} }
if (environments) {
await secretApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx);
await secretApprovalPolicyEnvironmentDAL.insertMany(
envs.map((env) => ({
envId: env.id,
policyId: doc.id
})),
tx
);
}
await secretApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx); await secretApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) { if (bypasserUserIds.length) {
@@ -441,7 +481,8 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
return { return {
...updatedSap, ...updatedSap,
environment: secretApprovalPolicy.environment, environments: secretApprovalPolicy.environments,
environment: secretApprovalPolicy.environments[0],
projectId: secretApprovalPolicy.projectId projectId: secretApprovalPolicy.projectId
}; };
}; };
@@ -487,7 +528,12 @@ export const secretApprovalPolicyServiceFactory = ({
const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx); const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx);
return updatedPolicy; return updatedPolicy;
}); });
return { ...deletedPolicy, projectId: sapPolicy.projectId, environment: sapPolicy.environment }; return {
...deletedPolicy,
projectId: sapPolicy.projectId,
environments: sapPolicy.environments,
environment: sapPolicy.environments[0]
};
}; };
const getSecretApprovalPolicyByProjectId = async ({ const getSecretApprovalPolicyByProjectId = async ({
@@ -520,7 +566,7 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
const policies = await secretApprovalPolicyDAL.find({ envId: env.id, deletedAt: null }); const policies = await secretApprovalPolicyDAL.find({ deletedAt: null }, { envId: env.id });
if (!policies.length) return; if (!policies.length) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path // this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter( const policiesFilteredByPath = policies.filter(
@@ -5,7 +5,8 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app
export type TCreateSapDTO = { export type TCreateSapDTO = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment?: string;
environments?: string[];
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: ( bypassers?: (
| { type: BypasserType.Group; id: string } | { type: BypasserType.Group; id: string }
@@ -29,6 +30,7 @@ export type TUpdateSapDTO = {
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals?: boolean; allowedSelfApprovals?: boolean;
environments?: string[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteSapDTO = { export type TDeleteSapDTO = {
@@ -40,6 +40,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.leftJoin(TableName.SecretApprovalPolicyEnvironment, (bd) => {
bd.on(
`${TableName.SecretApprovalPolicy}.id`,
"=",
`${TableName.SecretApprovalPolicyEnvironment}.policyId`
).andOn(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", `${TableName.SecretFolder}.envId`);
})
.leftJoin<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("statusChangedByUser"), db(TableName.Users).as("statusChangedByUser"),
`${TableName.SecretApprovalRequest}.statusChangedByUserId`, `${TableName.SecretApprovalRequest}.statusChangedByUserId`,
@@ -146,7 +153,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment), tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), tx.ref("envId").withSchema(TableName.SecretApprovalPolicyEnvironment).as("policyEnvId"),
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
@@ -538,6 +538,11 @@ export const secretApprovalRequestServiceFactory = ({
message: "The policy associated with this secret approval request has been deleted." message: "The policy associated with this secret approval request has been deleted."
}); });
} }
if (!policy.envId) {
throw new BadRequestError({
message: "The policy associated with this secret approval request is not linked to the environment."
});
}
const { hasRole } = await permissionService.getProjectPermission({ const { hasRole } = await permissionService.getProjectPermission({
actor: ActorType.USER, actor: ActorType.USER,
@@ -7,12 +7,13 @@ import {
TRotationFactoryRevokeCredentials, TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { import {
executeWithPotentialGateway, executeWithPotentialGateway,
SQL_CONNECTION_ALTER_LOGIN_STATEMENT SQL_CONNECTION_ALTER_LOGIN_STATEMENT
} from "@app/services/app-connection/shared/sql"; } from "@app/services/app-connection/shared/sql";
import { generatePassword } from "../utils"; import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../utils";
import { import {
TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection TSqlCredentialsRotationWithConnection
@@ -32,6 +33,11 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
return redactedMessage; return redactedMessage;
}; };
const ORACLE_PASSWORD_REQUIREMENTS = {
...DEFAULT_PASSWORD_REQUIREMENTS,
length: 30
};
export const sqlCredentialsRotationFactory: TRotationFactory< export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection, TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials TSqlCredentialsRotationGeneratedCredentials
@@ -43,6 +49,9 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
secretsMapping secretsMapping
} = secretRotation; } = secretRotation;
const passwordRequirement =
connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
const executeOperation = <T>( const executeOperation = <T>(
operation: (client: Knex) => Promise<T>, operation: (client: Knex) => Promise<T>,
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number] credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
@@ -65,7 +74,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
try { try {
await executeOperation(async (client) => { await executeOperation(async (client) => {
await client.raw("SELECT 1"); await client.raw(connection.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
}, credentials); }, credentials);
} catch (error) { } catch (error) {
throw new Error(redactPasswords(error, [credentials])); throw new Error(redactPasswords(error, [credentials]));
@@ -75,11 +84,13 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async ( const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
callback callback
) => { ) => {
// For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
// For SQL, since we get existing users, we change both their passwords // For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords // on issue to invalidate their existing passwords
const credentialsSet = [ const credentialsSet = [
{ username: username1, password: generatePassword() }, { username: username1, password: generatePassword(passwordRequirement) },
{ username: username2, password: generatePassword() } { username: username2, password: generatePassword(passwordRequirement) }
]; ];
try { try {
@@ -105,7 +116,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
credentialsToRevoke, credentialsToRevoke,
callback callback
) => { ) => {
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); const revokedCredentials = credentialsToRevoke.map(({ username }) => ({
username,
password: generatePassword(passwordRequirement)
}));
try { try {
await executeOperation(async (client) => { await executeOperation(async (client) => {
@@ -128,7 +142,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
callback callback
) => { ) => {
// generate new password for the next active user // generate new password for the next active user
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() }; const credentials = {
username: activeIndex === 0 ? username2 : username1,
password: generatePassword(passwordRequirement)
};
try { try {
await executeOperation(async (client) => { await executeOperation(async (client) => {
@@ -11,7 +11,7 @@ type TPasswordRequirements = {
allowedSymbols?: string; allowedSymbols?: string;
}; };
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = { export const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
length: 48, length: 48,
required: { required: {
lowercase: 1, lowercase: 1,
+7 -1
View File
@@ -2246,7 +2246,9 @@ export const AppConnections = {
}, },
AZURE_CLIENT_SECRETS: { AZURE_CLIENT_SECRETS: {
code: "The OAuth code to use to connect with Azure Client Secrets.", code: "The OAuth code to use to connect with Azure Client Secrets.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets." tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
clientId: "The Client ID to use to connect with Azure Client Secrets.",
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
}, },
AZURE_DEVOPS: { AZURE_DEVOPS: {
code: "The OAuth code to use to connect with Azure DevOps.", code: "The OAuth code to use to connect with Azure DevOps.",
@@ -2374,6 +2376,10 @@ export const SecretSyncs = {
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.", keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
tags: "Optional tags to add to secrets synced by Infisical.", tags: "Optional tags to add to secrets synced by Infisical.",
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.` syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
},
RENDER: {
autoRedeployServices:
"Whether Infisical should automatically redeploy the configured Render service upon secret changes."
} }
}, },
DESTINATION_CONFIG: { DESTINATION_CONFIG: {
+77 -6
View File
@@ -272,10 +272,26 @@ const envSchema = z
// gcp app // gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()), INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
// azure app // Legacy Single Multi Purpose Azure App Connection
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
// Azure App Configuration App Connection
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET: zpStr(z.string().optional()),
// Azure Key Vault App Connection
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET: zpStr(z.string().optional()),
// Azure Client Secrets App Connection
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET: zpStr(z.string().optional()),
// Azure DevOps App Connection
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()),
// datadog // datadog
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"), SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"), DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
@@ -353,7 +369,23 @@ const envSchema = z
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","), SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","),
PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0 PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0,
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID:
data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET:
data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID:
data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET:
data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID:
data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET:
data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID:
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET:
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET
})); }));
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>; export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
@@ -463,15 +495,54 @@ export const overwriteSchema: {
} }
] ]
}, },
azure: { azureAppConfiguration: {
name: "Azure", name: "Azure App Configuration",
fields: [ fields: [
{ {
key: "INF_APP_CONNECTION_AZURE_CLIENT_ID", key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID",
description: "The Application (Client) ID of your Azure application." description: "The Application (Client) ID of your Azure application."
}, },
{ {
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET", key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET",
description: "The Client Secret of your Azure application."
}
]
},
azureKeyVault: {
name: "Azure Key Vault",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID",
description: "The Application (Client) ID of your Azure application."
},
{
key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET",
description: "The Client Secret of your Azure application."
}
]
},
azureClientSecrets: {
name: "Azure Client Secrets",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID",
description: "The Application (Client) ID of your Azure application."
},
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET",
description: "The Client Secret of your Azure application."
}
]
},
azureDevOps: {
name: "Azure DevOps",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID",
description: "The Application (Client) ID of your Azure application."
},
{
key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET",
description: "The Client Secret of your Azure application." description: "The Client Secret of your Azure application."
} }
] ]
@@ -14,7 +14,7 @@ import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal
import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service"; import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service";
import { isBase64 } from "../../base64"; import { isBase64 } from "../../base64";
import { getConfig } from "../../config/env"; import { getConfig, TEnvConfig } from "../../config/env";
import { CryptographyError } from "../../errors"; import { CryptographyError } from "../../errors";
import { logger } from "../../logger"; import { logger } from "../../logger";
import { asymmetricFipsValidated } from "./asymmetric-fips"; import { asymmetricFipsValidated } from "./asymmetric-fips";
@@ -106,12 +106,12 @@ const cryptographyFactory = () => {
} }
}; };
const $setFipsModeEnabled = (enabled: boolean) => { const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
// If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key. // If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key.
if (enabled) { if (enabled) {
crypto.setFips(true); crypto.setFips(true);
const appCfg = getConfig(); const appCfg = envCfg || getConfig();
if (appCfg.ENCRYPTION_KEY) { if (appCfg.ENCRYPTION_KEY) {
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key // we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
@@ -141,14 +141,14 @@ const cryptographyFactory = () => {
$isInitialized = true; $isInitialized = true;
}; };
const initialize = async (superAdminDAL: TSuperAdminDALFactory) => { const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
if ($isInitialized) { if ($isInitialized) {
return isFipsModeEnabled(); return isFipsModeEnabled();
} }
if (process.env.FIPS_ENABLED !== "true") { if (process.env.FIPS_ENABLED !== "true") {
logger.info("Cryptography module initialized in normal operation mode."); logger.info("Cryptography module initialized in normal operation mode.");
$setFipsModeEnabled(false); $setFipsModeEnabled(false, envCfg);
return false; return false;
} }
@@ -158,11 +158,11 @@ const cryptographyFactory = () => {
if (serverCfg) { if (serverCfg) {
if (serverCfg.fipsEnabled) { if (serverCfg.fipsEnabled) {
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled."); logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
$setFipsModeEnabled(true); $setFipsModeEnabled(true, envCfg);
return true; return true;
} }
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS."); logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
$setFipsModeEnabled(false); $setFipsModeEnabled(false, envCfg);
return false; return false;
} }
@@ -171,7 +171,7 @@ const cryptographyFactory = () => {
// TODO(daniel): check if it's an enterprise deployment // TODO(daniel): check if it's an enterprise deployment
// if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true. // if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true.
$setFipsModeEnabled(true); $setFipsModeEnabled(true, envCfg);
return true; return true;
}; };
+9 -1
View File
@@ -11,6 +11,7 @@ import {
accessApprovalPolicyBypasserDALFactory accessApprovalPolicyBypasserDALFactory
} from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
import { accessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal";
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal"; import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal"; import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
@@ -76,6 +77,7 @@ import {
secretApprovalPolicyBypasserDALFactory secretApprovalPolicyBypasserDALFactory
} from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal"; } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal"; import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
import { secretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal";
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal"; import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal";
@@ -425,9 +427,11 @@ export const registerRoutes = async (
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db); const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db);
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db); const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
const accessApprovalPolicyEnvironmentDAL = accessApprovalPolicyEnvironmentDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db); const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db);
const sapEnvironmentDAL = secretApprovalPolicyEnvironmentDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db); const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
@@ -561,6 +565,7 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL, secretApprovalPolicyApproverDAL: sapApproverDAL,
secretApprovalPolicyBypasserDAL: sapBypasserDAL, secretApprovalPolicyBypasserDAL: sapBypasserDAL,
secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL,
permissionService, permissionService,
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
licenseService, licenseService,
@@ -1156,7 +1161,9 @@ export const registerRoutes = async (
keyStore, keyStore,
licenseService, licenseService,
projectDAL, projectDAL,
folderDAL folderDAL,
accessApprovalPolicyEnvironmentDAL,
secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL
}); });
const projectRoleService = projectRoleServiceFactory({ const projectRoleService = projectRoleServiceFactory({
@@ -1318,6 +1325,7 @@ export const registerRoutes = async (
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL, accessApprovalPolicyBypasserDAL,
accessApprovalPolicyEnvironmentDAL,
groupDAL, groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
@@ -93,6 +93,13 @@ export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
name: z.string(), name: z.string(),
slug: z.string() slug: z.string()
}), }),
environments: z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string()
})
),
projectId: z.string() projectId: z.string()
}) })
); );
@@ -14,13 +14,13 @@ import {
} from "./azure-app-configuration-connection-types"; } from "./azure-app-configuration-connection-types";
export const getAzureAppConfigurationConnectionListItem = () => { export const getAzureAppConfigurationConnectionListItem = () => {
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); const { INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID } = getConfig();
return { return {
name: "Azure App Configuration" as const, name: "Azure App Configuration" as const,
app: AppConnection.AzureAppConfiguration as const, app: AppConnection.AzureAppConfiguration as const,
methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth], methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID oauthClientId: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID
}; };
}; };
@@ -29,9 +29,16 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
) => { ) => {
const { credentials: inputCredentials, method } = config; const { credentials: inputCredentials, method } = config;
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); const {
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET,
SITE_URL
} = getConfig();
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { if (
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID ||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET
) {
throw new InternalServerError({ throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
}); });
@@ -47,8 +54,8 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
grant_type: "authorization_code", grant_type: "authorization_code",
code: inputCredentials.code, code: inputCredentials.code,
scope: `openid offline_access https://azconfig.io/.default`, scope: `openid offline_access https://azconfig.io/.default`,
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
}) })
); );
@@ -1,3 +1,4 @@
export enum AzureClientSecretsConnectionMethod { export enum AzureClientSecretsConnectionMethod {
OAuth = "oauth" OAuth = "oauth",
ClientSecret = "client-secret"
} }
@@ -1,3 +1,4 @@
/* eslint-disable no-case-declarations */
import { AxiosError, AxiosResponse } from "axios"; import { AxiosError, AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
@@ -16,18 +17,22 @@ import { AppConnection } from "../app-connection-enums";
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums"; import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
import { import {
ExchangeCodeAzureResponse, ExchangeCodeAzureResponse,
TAzureClientSecretsConnectionClientSecretCredentials,
TAzureClientSecretsConnectionConfig, TAzureClientSecretsConnectionConfig,
TAzureClientSecretsConnectionCredentials TAzureClientSecretsConnectionCredentials
} from "./azure-client-secrets-connection-types"; } from "./azure-client-secrets-connection-types";
export const getAzureClientSecretsConnectionListItem = () => { export const getAzureClientSecretsConnectionListItem = () => {
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); const { INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID } = getConfig();
return { return {
name: "Azure Client Secrets" as const, name: "Azure Client Secrets" as const,
app: AppConnection.AzureClientSecrets as const, app: AppConnection.AzureClientSecrets as const,
methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth], methods: Object.values(AzureClientSecretsConnectionMethod) as [
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID AzureClientSecretsConnectionMethod.OAuth,
AzureClientSecretsConnectionMethod.ClientSecret
],
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID
}; };
}; };
@@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async (
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => { ) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
throw new BadRequestError({
message: `Azure environment variables have not been configured`
});
}
const appConnection = await appConnectionDAL.findById(connectionId); const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) { if (!appConnection) {
@@ -63,104 +62,195 @@ export const getAzureConnectionAccessToken = async (
const { refreshToken } = credentials; const { refreshToken } = credentials;
const currentTime = Date.now(); const currentTime = Date.now();
switch (appConnection.method) {
case AzureClientSecretsConnectionMethod.OAuth:
if (
!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID ||
!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET
) {
throw new BadRequestError({
message: `Azure OAuth environment variables have not been configured`
});
}
const { data } = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
new URLSearchParams({
grant_type: "refresh_token",
scope: `openid offline_access https://graph.microsoft.com/.default`,
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID,
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET,
refresh_token: refreshToken
})
);
const { data } = await request.post<ExchangeCodeAzureResponse>( const updatedCredentials = {
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"), ...credentials,
new URLSearchParams({ accessToken: data.access_token,
grant_type: "refresh_token", expiresAt: currentTime + data.expires_in * 1000,
scope: `openid offline_access https://graph.microsoft.com/.default`, refreshToken: data.refresh_token
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, };
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
refresh_token: refreshToken
})
);
const updatedCredentials = { const encryptedCredentials = await encryptAppConnectionCredentials({
...credentials, credentials: updatedCredentials,
accessToken: data.access_token, orgId: appConnection.orgId,
expiresAt: currentTime + data.expires_in * 1000, kmsService
refreshToken: data.refresh_token });
};
const encryptedCredentials = await encryptAppConnectionCredentials({ await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
credentials: updatedCredentials,
orgId: appConnection.orgId,
kmsService
});
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials }); return data.access_token;
case AzureClientSecretsConnectionMethod.ClientSecret:
const accessTokenCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId,
kmsService,
encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureClientSecretsConnectionClientSecretCredentials;
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = accessTokenCredentials;
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
return accessToken;
}
return data.access_token; const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
new URLSearchParams({
grant_type: "client_credentials",
scope: `https://graph.microsoft.com/.default`,
client_id: clientId,
client_secret: clientSecret
})
);
const updatedClientCredentials = {
...accessTokenCredentials,
accessToken: clientData.access_token,
expiresAt: currentTime + clientData.expires_in * 1000
};
const encryptedClientCredentials = await encryptAppConnectionCredentials({
credentials: updatedClientCredentials,
orgId: appConnection.orgId,
kmsService
});
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
return clientData.access_token;
default:
throw new InternalServerError({
message: `Unhandled Azure connection method: ${appConnection.method as AzureClientSecretsConnectionMethod}`
});
}
}; };
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => { export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
const { credentials: inputCredentials, method } = config; const { credentials: inputCredentials, method } = config;
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); const {
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID,
if (!SITE_URL) { INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET,
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); SITE_URL
} } = getConfig();
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
let tokenError: AxiosError | null = null;
try {
tokenResp = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
new URLSearchParams({
grant_type: "authorization_code",
code: inputCredentials.code,
scope: `openid offline_access https://graph.microsoft.com/.default`,
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
} catch (e: unknown) {
if (e instanceof AxiosError) {
tokenError = e;
} else {
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
}
}
if (tokenError) {
if (tokenError instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to get access token: ${
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
}`
});
} else {
throw new InternalServerError({
message: "Failed to get access token"
});
}
}
if (!tokenResp) {
throw new InternalServerError({
message: `Failed to get access token: Token was empty with no error`
});
}
switch (method) { switch (method) {
case AzureClientSecretsConnectionMethod.OAuth: case AzureClientSecretsConnectionMethod.OAuth:
if (!SITE_URL) {
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
}
if (
!INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID ||
!INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET
) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
let tokenError: AxiosError | null = null;
try {
tokenResp = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
new URLSearchParams({
grant_type: "authorization_code",
code: inputCredentials.code,
scope: `openid offline_access https://graph.microsoft.com/.default`,
client_id: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID,
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
} catch (e: unknown) {
if (e instanceof AxiosError) {
tokenError = e;
} else {
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
}
}
if (tokenError) {
if (tokenError instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to get access token: ${
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
}`
});
} else {
throw new InternalServerError({
message: "Failed to get access token"
});
}
}
if (!tokenResp) {
throw new InternalServerError({
message: `Failed to get access token: Token was empty with no error`
});
}
return { return {
tenantId: inputCredentials.tenantId, tenantId: inputCredentials.tenantId,
accessToken: tokenResp.data.access_token, accessToken: tokenResp.data.access_token,
refreshToken: tokenResp.data.refresh_token, refreshToken: tokenResp.data.refresh_token,
expiresAt: Date.now() + tokenResp.data.expires_in * 1000 expiresAt: Date.now() + tokenResp.data.expires_in * 1000
}; };
case AzureClientSecretsConnectionMethod.ClientSecret:
const { tenantId, clientId, clientSecret } = inputCredentials;
try {
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
new URLSearchParams({
grant_type: "client_credentials",
scope: `https://graph.microsoft.com/.default`,
client_id: clientId,
client_secret: clientSecret
})
);
return {
tenantId,
accessToken: clientData.access_token,
expiresAt: Date.now() + clientData.expires_in * 1000,
clientId,
clientSecret
};
} catch (e: unknown) {
if (e instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to get access token: ${
(e?.response?.data as { error_description?: string })?.error_description || "Unknown error"
}`
});
} else {
throw new InternalServerError({
message: "Failed to get access token"
});
}
}
default: default:
throw new InternalServerError({ throw new InternalServerError({
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}` message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
@@ -26,6 +26,36 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
expiresAt: z.number() expiresAt: z.number()
}); });
export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z.object({
clientId: z
.string()
.uuid()
.trim()
.min(1, "Client ID required")
.max(50, "Client ID must be at most 50 characters long")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId),
clientSecret: z
.string()
.trim()
.min(1, "Client Secret required")
.max(50, "Client Secret must be at most 50 characters long")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret),
tenantId: z
.string()
.uuid()
.trim()
.min(1, "Tenant ID required")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
});
export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = z.object({
clientId: z.string(),
clientSecret: z.string(),
tenantId: z.string(),
accessToken: z.string(),
expiresAt: z.number()
});
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({ z.object({
method: z method: z
@@ -34,6 +64,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe( credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
) )
}),
z.object({
method: z
.literal(AzureClientSecretsConnectionMethod.ClientSecret)
.describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method),
credentials: AzureClientSecretsConnectionClientSecretInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
)
}) })
]); ]);
@@ -43,9 +81,13 @@ export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecre
export const UpdateAzureClientSecretsConnectionSchema = z export const UpdateAzureClientSecretsConnectionSchema = z
.object({ .object({
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe( credentials: z
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials .union([
) AzureClientSecretsConnectionOAuthInputCredentialsSchema,
AzureClientSecretsConnectionClientSecretInputCredentialsSchema
])
.optional()
.describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
@@ -59,6 +101,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
z.object({ z.object({
method: z.literal(AzureClientSecretsConnectionMethod.OAuth), method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
}),
z.object({
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
}) })
]) ])
); );
@@ -69,6 +115,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({ credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true tenantId: true
}) })
}),
BaseAzureClientSecretsConnectionSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true,
tenantId: true
})
}) })
]); ]);
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { import {
AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
AzureClientSecretsConnectionOAuthOutputCredentialsSchema, AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
AzureClientSecretsConnectionSchema, AzureClientSecretsConnectionSchema,
CreateAzureClientSecretsConnectionSchema, CreateAzureClientSecretsConnectionSchema,
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
>; >;
export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
>;
export interface ExchangeCodeAzureResponse { export interface ExchangeCodeAzureResponse {
token_type: string; token_type: string;
scope: string; scope: string;
@@ -23,7 +23,7 @@ import {
} from "./azure-devops-types"; } from "./azure-devops-types";
export const getAzureDevopsConnectionListItem = () => { export const getAzureDevopsConnectionListItem = () => {
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); const { INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID } = getConfig();
return { return {
name: "Azure DevOps" as const, name: "Azure DevOps" as const,
@@ -32,7 +32,7 @@ export const getAzureDevopsConnectionListItem = () => {
AzureDevOpsConnectionMethod.OAuth, AzureDevOpsConnectionMethod.OAuth,
AzureDevOpsConnectionMethod.AccessToken AzureDevOpsConnectionMethod.AccessToken
], ],
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID oauthClientId: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID
}; };
}; };
@@ -63,7 +63,7 @@ export const getAzureDevopsConnection = async (
switch (appConnection.method) { switch (appConnection.method) {
case AzureDevOpsConnectionMethod.OAuth: case AzureDevOpsConnectionMethod.OAuth:
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { if (!appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET) {
throw new BadRequestError({ throw new BadRequestError({
message: `Azure environment variables have not been configured` message: `Azure environment variables have not been configured`
}); });
@@ -81,8 +81,8 @@ export const getAzureDevopsConnection = async (
new URLSearchParams({ new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
scope: `https://app.vssps.visualstudio.com/.default`, scope: `https://app.vssps.visualstudio.com/.default`,
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, client_id: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID,
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, client_secret: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET,
refresh_token: refreshToken refresh_token: refreshToken
}) })
); );
@@ -119,7 +119,8 @@ export const getAzureDevopsConnection = async (
export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => { export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => {
const { credentials: inputCredentials, method } = config; const { credentials: inputCredentials, method } = config;
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); const { INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID, INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET, SITE_URL } =
getConfig();
switch (method) { switch (method) {
case AzureDevOpsConnectionMethod.OAuth: case AzureDevOpsConnectionMethod.OAuth:
@@ -127,7 +128,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
} }
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { if (!INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || !INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET) {
throw new InternalServerError({ throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
}); });
@@ -144,8 +145,8 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
grant_type: "authorization_code", grant_type: "authorization_code",
code: oauthCredentials.code, code: oauthCredentials.code,
scope: `https://app.vssps.visualstudio.com/.default`, scope: `https://app.vssps.visualstudio.com/.default`,
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, client_id: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID,
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, client_secret: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
}) })
); );
@@ -26,7 +26,10 @@ export const getAzureConnectionAccessToken = async (
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => { ) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { if (
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID ||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET
) {
throw new BadRequestError({ throw new BadRequestError({
message: `Azure environment variables have not been configured` message: `Azure environment variables have not been configured`
}); });
@@ -57,8 +60,8 @@ export const getAzureConnectionAccessToken = async (
new URLSearchParams({ new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
scope: `openid offline_access`, scope: `openid offline_access`,
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
refresh_token: credentials.refreshToken refresh_token: credentials.refreshToken
}) })
); );
@@ -92,22 +95,23 @@ export const getAzureConnectionAccessToken = async (
}; };
export const getAzureKeyVaultConnectionListItem = () => { export const getAzureKeyVaultConnectionListItem = () => {
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID } = getConfig();
return { return {
name: "Azure Key Vault" as const, name: "Azure Key Vault" as const,
app: AppConnection.AzureKeyVault as const, app: AppConnection.AzureKeyVault as const,
methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth], methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID oauthClientId: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID
}; };
}; };
export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => { export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => {
const { credentials: inputCredentials, method } = config; const { credentials: inputCredentials, method } = config;
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, SITE_URL } =
getConfig();
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { if (!INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || !INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET) {
throw new InternalServerError({ throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
}); });
@@ -123,8 +127,8 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
grant_type: "authorization_code", grant_type: "authorization_code",
code: inputCredentials.code, code: inputCredentials.code,
scope: `openid offline_access https://vault.azure.net/.default`, scope: `openid offline_access https://vault.azure.net/.default`,
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
}) })
); );
@@ -1,9 +1,11 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TAccessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -20,6 +22,8 @@ type TProjectEnvServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
accessApprovalPolicyEnvironmentDAL: Pick<TAccessApprovalPolicyEnvironmentDALFactory, "findAvailablePoliciesByEnvId">;
secretApprovalPolicyEnvironmentDAL: Pick<TSecretApprovalPolicyEnvironmentDALFactory, "findAvailablePoliciesByEnvId">;
}; };
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>; export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
@@ -30,7 +34,9 @@ export const projectEnvServiceFactory = ({
licenseService, licenseService,
keyStore, keyStore,
projectDAL, projectDAL,
folderDAL folderDAL,
accessApprovalPolicyEnvironmentDAL,
secretApprovalPolicyEnvironmentDAL
}: TProjectEnvServiceFactoryDep) => { }: TProjectEnvServiceFactoryDep) => {
const createEnvironment = async ({ const createEnvironment = async ({
projectId, projectId,
@@ -220,6 +226,20 @@ export const projectEnvServiceFactory = ({
} }
const env = await projectEnvDAL.transaction(async (tx) => { const env = await projectEnvDAL.transaction(async (tx) => {
const secretApprovalPolicies = await secretApprovalPolicyEnvironmentDAL.findAvailablePoliciesByEnvId(id, tx);
if (secretApprovalPolicies.length > 0) {
throw new BadRequestError({
message: "Environment is in use by a secret approval policy",
name: "DeleteEnvironment"
});
}
const accessApprovalPolicies = await accessApprovalPolicyEnvironmentDAL.findAvailablePoliciesByEnvId(id, tx);
if (accessApprovalPolicies.length > 0) {
throw new BadRequestError({
message: "Environment is in use by an access approval policy",
name: "DeleteEnvironment"
});
}
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx); const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
if (!doc) if (!doc)
throw new NotFoundError({ throw new NotFoundError({
@@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({
{ pattern: "0 */1 * * *", utc: true }, { pattern: "0 */1 * * *", utc: true },
QueueName.SecretReminderMigration // just a job id QueueName.SecretReminderMigration // just a job id
); );
await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, {
delay: 5000,
jobId: QueueName.SecretReminderMigration,
repeat: { pattern: "0 */1 * * *", utc: true }
});
}; };
queueService.listen(QueueName.DailyReminders, "failed", (_, err) => { queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {
@@ -97,6 +97,28 @@ const batchUpdateEnvironmentSecrets = async (
); );
}; };
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
const {
destinationConfig,
connection: {
credentials: { apiKey }
}
} = secretSync;
await makeRequestWithRetry(() =>
request.post(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
{},
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
)
);
};
export const RenderSyncFns = { export const RenderSyncFns = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync); const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
@@ -131,6 +153,10 @@ export const RenderSyncFns = {
} }
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
if (secretSync.syncOptions.autoRedeployServices) {
await redeployService(secretSync);
}
}, },
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => { getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => {
@@ -151,5 +177,9 @@ export const RenderSyncFns = {
} }
} }
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
if (secretSync.syncOptions.autoRedeployServices) {
await redeployService(secretSync);
}
} }
}; };
@@ -20,23 +20,33 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
}) })
]); ]);
const RenderSyncOptionsSchema = z.object({
autoRedeployServices: z.boolean().optional().describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.RENDER.autoRedeployServices)
});
const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({ export const RenderSyncSchema = BaseSecretSyncSchema(
SecretSync.Render,
RenderSyncOptionsConfig,
RenderSyncOptionsSchema
).extend({
destination: z.literal(SecretSync.Render), destination: z.literal(SecretSync.Render),
destinationConfig: RenderSyncDestinationConfigSchema destinationConfig: RenderSyncDestinationConfigSchema
}); });
export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema( export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Render, SecretSync.Render,
RenderSyncOptionsConfig RenderSyncOptionsConfig,
RenderSyncOptionsSchema
).extend({ ).extend({
destinationConfig: RenderSyncDestinationConfigSchema destinationConfig: RenderSyncDestinationConfigSchema
}); });
export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema( export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Render, SecretSync.Render,
RenderSyncOptionsConfig RenderSyncOptionsConfig,
RenderSyncOptionsSchema
).extend({ ).extend({
destinationConfig: RenderSyncDestinationConfigSchema.optional() destinationConfig: RenderSyncDestinationConfigSchema.optional()
}); });
Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

@@ -50,8 +50,8 @@ Infisical currently only supports one method for connecting to Azure, which is O
Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. Back in your Infisical instance, add two new environment variables for the credentials of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - `INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID`: The **Application (Client) ID** of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. - `INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET`: The **Client Secret** of your Azure application.
Once added, restart your Infisical instance and use the Azure App Configuration connection. Once added, restart your Infisical instance and use the Azure App Configuration connection.
</Step> </Step>
@@ -43,12 +43,6 @@ Infisical currently only supports one method for connecting to Azure, which is O
- `Application.ReadWrite.All` (Delegated) - `Application.ReadWrite.All` (Delegated)
- `Directory.ReadWrite.All` (Delegated) - `Directory.ReadWrite.All` (Delegated)
- `User.Read` (Delegated) - `User.Read` (Delegated)
- Azure App Configuration
- `KeyValue.Delete` (Delegated)
- `KeyValue.Read` (Delegated)
- `KeyValue.Write` (Delegated)
- Access Key Vault
- `user_impersonation` (Delegated)
![Azure client secrets](/images/integrations/azure-client-secrets/app-api-permissions.png) ![Azure client secrets](/images/integrations/azure-client-secrets/app-api-permissions.png)
@@ -63,8 +57,8 @@ Infisical currently only supports one method for connecting to Azure, which is O
Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. Back in your Infisical instance, add two new environment variables for the credentials of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID`: The **Application (Client) ID** of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET`: The **Client Secret** of your Azure application.
Once added, restart your Infisical instance and use the Azure Client Secrets connection. Once added, restart your Infisical instance and use the Azure Client Secrets connection.
</Step> </Step>
@@ -72,6 +66,30 @@ Infisical currently only supports one method for connecting to Azure, which is O
</Accordion> </Accordion>
<Accordion title="Client Secret Authentication">
Ensure your Azure application has the required permissions that Infisical needs for the Azure Client Secrets connection to work.
**Prerequisites:**
- An active Azure setup.
<Steps>
<Step title="Assign API permissions to the application">
For the Azure Client Secrets connection to work, assign the following permissions to your Azure application:
#### Required API Permissions
**Microsoft Graph**
- `Application.ReadWrite.All`
- `Application.ReadWrite.OwnedBy`
- `Application.ReadWrite.All` (Delegated)
- `Directory.ReadWrite.All` (Delegated)
- `User.Read` (Delegated)
![Azure client secrets](/images/integrations/azure-client-secrets/app-api-permissions.png)
</Step>
</Steps>
</Accordion>
## Setup Azure Connection in Infisical ## Setup Azure Connection in Infisical
<Steps> <Steps>
@@ -82,21 +100,31 @@ Infisical currently only supports one method for connecting to Azure, which is O
<Step title="Add Connection"> <Step title="Add Connection">
Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/client-secrets/select-connection.png) Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/client-secrets/select-connection.png)
</Step> </Step>
<Step title="Authorize Connection"> <Step title="Create Connection">
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step. <Tabs>
<Tab title="OAuth">
<Step title="Authorize Connection">
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
Now select the **OAuth** method and click **Connect to Azure**. Now select the **OAuth** method and click **Connect to Azure**.
![Connect via Azure OAUth](/images/app-connections/azure/client-secrets/create-oauth-method.png) ![Connect via Azure OAUth](/images/app-connections/azure/client-secrets/create-oauth-method.png)
</Step>
<Step title="Grant Access">
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
you will be redirected back to Infisical's App Connections page. ![Azure Client Secrets
Authorization](/images/app-connections/azure/grant-access.png)
</Step>
</Tab>
<Tab title="Client Secret">
<Step title="Create Connection">
Fill in the **Tenant ID**, **Client ID** and **Client Secret** fields with the Directory (Tenant) ID, Application (Client) ID and Client Secret you obtained in the previous step.
![Connect via Azure OAUth](/images/app-connections/azure/client-secrets/create-client-secrets-method.png)
</Step>
</Step> </Tab>
<Step title="Grant Access"> </Tabs>
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, </Step>
you will be redirected back to Infisical's App Connections page. ![Azure Client Secrets
Authorization](/images/app-connections/azure/grant-access.png)
</Step>
<Step title="Connection Created"> <Step title="Connection Created">
Your **Azure Client Secrets Connection** is now available for use. ![Azure Client Secrets](/images/app-connections/azure/client-secrets/oauth-connection.png) Your **Azure Client Secrets Connection** is now available for use. ![Azure Client Secrets](/images/app-connections/azure/client-secrets/oauth-connection.png)
</Step> </Step>
@@ -56,8 +56,8 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a
Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. Back in your Infisical instance, add two new environment variables for the credentials of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - `INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID`: The **Application (Client) ID** of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. - `INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET`: The **Client Secret** of your Azure application.
Once added, restart your Infisical instance and use the Azure Client Secrets connection. Once added, restart your Infisical instance and use the Azure Client Secrets connection.
</Step> </Step>
@@ -49,8 +49,8 @@ Infisical currently only supports one method for connecting to Azure, which is O
Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. Back in your Infisical instance, add two new environment variables for the credentials of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - `INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID`: The **Application (Client) ID** of your Azure application.
- `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. - `INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET`: The **Client Secret** of your Azure application.
Once added, restart your Infisical instance and use the Azure Key Vault connection. Once added, restart your Infisical instance and use the Azure Key Vault connection.
</Step> </Step>
+16 -24
View File
@@ -55,7 +55,7 @@
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
"axios": "^1.7.9", "axios": "^1.11.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"cva": "npm:class-variance-authority@^0.7.1", "cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -5282,13 +5282,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.3", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -5700,7 +5700,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -6665,7 +6664,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
"integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.0", "call-bind-apply-helpers": "^1.0.0",
@@ -6819,7 +6817,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6829,7 +6826,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6867,7 +6863,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@@ -6877,15 +6872,15 @@
} }
}, },
"node_modules/es-set-tostringtag": { "node_modules/es-set-tostringtag": {
"version": "2.0.3", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.4", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2", "has-tostringtag": "^1.0.2",
"hasown": "^2.0.1" "hasown": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -7855,13 +7850,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.1", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@@ -7992,7 +7989,6 @@
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@@ -8139,7 +8135,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -8215,7 +8210,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -8228,7 +8222,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -9545,7 +9538,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
"integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
+1 -1
View File
@@ -59,7 +59,7 @@
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
"axios": "^1.7.9", "axios": "^1.11.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"cva": "npm:class-variance-authority@^0.7.1", "cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -1,26 +1,14 @@
import { FunctionComponent, ReactNode } from "react"; import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react"; import { BoundCanProps, Can } from "@casl/react";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext"; import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
import { Tooltip } from "../v2"; import { AccessRestrictedBanner, Tooltip } from "../v2";
export const OrgPermissionGuardBanner = () => { export const OrgPermissionGuardBanner = () => {
return ( return (
<div className="container mx-auto flex h-full items-center justify-center"> <div className="container mx-auto flex h-full items-center justify-center">
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300"> <AccessRestrictedBanner />
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
<div className="text-sm">
Your role has limited permissions, please <br /> contact your admin to gain access
</div>
</div>
</div>
</div> </div>
); );
}; };
@@ -1,15 +1,12 @@
import { ReactNode } from "react";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { AccessRestrictedBanner } from "@app/components/v2";
type Props = { type Props = {
containerClassName?: string; containerClassName?: string;
className?: string;
children?: ReactNode;
}; };
export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => { export const PermissionDeniedBanner = ({ containerClassName }: Props) => {
return ( return (
<div <div
className={twMerge( className={twMerge(
@@ -17,22 +14,7 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
containerClassName containerClassName
)} )}
> >
<div className={twMerge("rounded-md bg-mineshaft-800 p-16 text-bunker-300", className)}> <AccessRestrictedBanner />
<div className="flex items-end space-x-12">
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
{children || (
<div className="text-sm">
Your role has limited permissions, please <br /> contact your administrator to gain
access
</div>
)}
</div>
</div>
</div>
</div> </div>
); );
}; };
@@ -1,9 +1,8 @@
import { FunctionComponent, ReactNode } from "react"; import { FunctionComponent, ReactNode } from "react";
import { AbilityTuple, MongoAbility } from "@casl/ability"; import { AbilityTuple, MongoAbility } from "@casl/ability";
import { Can } from "@casl/react"; import { Can } from "@casl/react";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AccessRestrictedBanner } from "@app/components/v2";
import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext";
import { Tooltip } from "../v2/Tooltip"; import { Tooltip } from "../v2/Tooltip";
@@ -11,17 +10,7 @@ import { Tooltip } from "../v2/Tooltip";
export const ProjectPermissionGuardBanner = () => { export const ProjectPermissionGuardBanner = () => {
return ( return (
<div className="container mx-auto flex h-full items-center justify-center"> <div className="container mx-auto flex h-full items-center justify-center">
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300"> <AccessRestrictedBanner />
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
<div className="text-sm">
Your role has limited permissions, please <br /> contact your admin to gain access
</div>
</div>
</div>
</div> </div>
); );
}; };
@@ -76,7 +76,6 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
</div> </div>
) )
} }
onPointerDownOutside={(e) => e.preventDefault()}
className={selectedRotation ? "max-w-2xl" : "max-w-3xl"} className={selectedRotation ? "max-w-2xl" : "max-w-3xl"}
subTitle={ subTitle={
selectedRotation ? undefined : "Select a provider to create a secret rotation for." selectedRotation ? undefined : "Select a provider to create a secret rotation for."
@@ -75,7 +75,6 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
</div> </div>
) )
} }
onPointerDownOutside={(e) => e.preventDefault()}
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"} className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
subTitle={ subTitle={
selectedDataSource ? undefined : "Select a data source to configure secret scanning for." selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
@@ -56,7 +56,6 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
"Add Sync" "Add Sync"
) )
} }
onPointerDownOutside={(e) => e.preventDefault()}
className="max-w-2xl" className="max-w-2xl"
bodyClassName="overflow-visible" bodyClassName="overflow-visible"
subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."} subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."}
@@ -9,7 +9,7 @@ import {
useRenderConnectionListServices useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render"; } from "@app/hooks/api/appConnections/render";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
import { TSecretSyncForm } from "../schemas"; import { TSecretSyncForm } from "../schemas";
@@ -0,0 +1,40 @@
import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Switch, Tooltip } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const RenderSyncOptionsFields = () => {
const { control } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
return (
<Controller
name="syncOptions.autoRedeployServices"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl className="mt-4" isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="auto-redeploy-services"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Auto Redeploy Services On Sync
<Tooltip
className="max-w-md"
content={
<p>If enabled, services will be automatically redeployed upon secret changes.</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</Switch>
</FormControl>
)}
/>
);
};
@@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas"; import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields"; import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields"; import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields";
type Props = { type Props = {
hideInitialSync?: boolean; hideInitialSync?: boolean;
@@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.AWSSecretsManager: case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />; AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
break; break;
case SecretSync.Render:
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsFields />;
break;
case SecretSync.GitHub: case SecretSync.GitHub:
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
@@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.OnePass: case SecretSync.OnePass:
case SecretSync.OCIVault: case SecretSync.OCIVault:
case SecretSync.Heroku: case SecretSync.Heroku:
case SecretSync.Render:
case SecretSync.Flyio: case SecretSync.Flyio:
case SecretSync.GitLab: case SecretSync.GitLab:
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
@@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs"; import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const RenderSyncOptionsReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
const [{ autoRedeployServices }] = watch(["syncOptions"]);
return (
<div>
{autoRedeployServices ? (
<GenericFieldLabel label="Auto Redeploy Services">
<Badge variant="success">Enabled</Badge>
</GenericFieldLabel>
) : (
<GenericFieldLabel label="Auto Redeploy Services">
<Badge variant="danger">Disabled</Badge>
</GenericFieldLabel>
)}
</div>
);
};
export const RenderSyncReviewFields = () => { export const RenderSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>(); const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
const serviceName = watch("destinationConfig.serviceName"); const serviceName = watch("destinationConfig.serviceName");
@@ -35,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields"; import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields"; import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields"; import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
import { RenderSyncReviewFields } from "./RenderSyncReviewFields"; import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields";
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields"; import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
@@ -121,6 +121,7 @@ export const SecretSyncReviewFields = () => {
break; break;
case SecretSync.Render: case SecretSync.Render:
DestinationFieldsComponent = <RenderSyncReviewFields />; DestinationFieldsComponent = <RenderSyncReviewFields />;
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsReviewFields />;
break; break;
case SecretSync.Flyio: case SecretSync.Flyio:
DestinationFieldsComponent = <FlyioSyncReviewFields />; DestinationFieldsComponent = <FlyioSyncReviewFields />;
@@ -2,9 +2,13 @@ import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge( export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
z.object({
autoRedeployServices: z.boolean().optional()
})
).merge(
z.object({ z.object({
destination: z.literal(SecretSync.Render), destination: z.literal(SecretSync.Render),
destinationConfig: z.discriminatedUnion("scope", [ destinationConfig: z.discriminatedUnion("scope", [
@@ -0,0 +1,26 @@
import { ReactNode } from "react";
type Props = {
title?: string;
body?: ReactNode;
};
export const AccessRestrictedBanner = ({
title = "Access Restricted",
body = (
<>
Your current role doesn&#39;t provide access to this feature.
<br /> Contact your administrator to request access.
</>
)
}: Props) => {
return (
<div className="flex items-center rounded-md border border-mineshaft-500 bg-gradient-to-br from-mineshaft-900 to-mineshaft-600 px-16 py-12 text-center text-bunker-300">
<div>
<div className="text-4xl font-medium text-bunker-100">{title}</div>
<div className="-mt-1 text-sm">{body}</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./AccessRestrictedBanner";
+1
View File
@@ -1,4 +1,5 @@
/* eslint-disable react-refresh/only-export-components */ /* eslint-disable react-refresh/only-export-components */
export * from "./AccessRestrictedBanner";
export * from "./Accordion"; export * from "./Accordion";
export * from "./Alert"; export * from "./Alert";
export * from "./Badge"; export * from "./Badge";
+2 -1
View File
@@ -171,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case RenderConnectionMethod.ApiKey: case RenderConnectionMethod.ApiKey:
case ChecklyConnectionMethod.ApiKey: case ChecklyConnectionMethod.ApiKey:
return { name: "API Key", icon: faKey }; return { name: "API Key", icon: faKey };
case AzureClientSecretsConnectionMethod.ClientSecret:
return { name: "Client Secret", icon: faKey };
default: default:
throw new Error(`Unhandled App Connection Method: ${method}`); throw new Error(`Unhandled App Connection Method: ${method}`);
} }
+1 -1
View File
@@ -4,9 +4,9 @@ import {
SecretSyncImportBehavior, SecretSyncImportBehavior,
SecretSyncInitialSyncBehavior SecretSyncInitialSyncBehavior
} from "@app/hooks/api/secretSyncs"; } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/render-sync";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = { export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" }, [SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
@@ -1,9 +1,8 @@
import { ComponentType } from "react"; import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability"; import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { AccessRestrictedBanner } from "@app/components/v2";
import { TOrgPermission, useOrgPermission } from "@app/context"; import { TOrgPermission, useOrgPermission } from "@app/context";
type Props<T extends Abilities> = (T extends AbilityTuple type Props<T extends Abilities> = (T extends AbilityTuple
@@ -14,11 +13,11 @@ type Props<T extends Abilities> = (T extends AbilityTuple
: { : {
action: string; action: string;
subject: string; subject: string;
}) & { className?: string; containerClassName?: string }; }) & { containerClassName?: string };
export const withPermission = <T extends object, J extends TOrgPermission>( export const withPermission = <T extends object, J extends TOrgPermission>(
Component: ComponentType<T>, Component: ComponentType<T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]> { action, subject, containerClassName }: Props<Generics<J>["abilities"]>
) => { ) => {
const HOC = (hocProps: T) => { const HOC = (hocProps: T) => {
const { permission } = useOrgPermission(); const { permission } = useOrgPermission();
@@ -33,22 +32,7 @@ export const withPermission = <T extends object, J extends TOrgPermission>(
containerClassName containerClassName
)} )}
> >
<div <AccessRestrictedBanner />
className={twMerge(
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
<div className="text-sm">
Your role has limited permissions, please <br /> contact your admin to gain access
</div>
</div>
</div>
</div> </div>
); );
} }
@@ -1,14 +1,12 @@
import { ComponentType } from "react"; import { ComponentType } from "react";
import { AbilityTuple } from "@casl/ability"; import { AbilityTuple } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { AccessRestrictedBanner } from "@app/components/v2";
import { useProjectPermission } from "@app/context"; import { useProjectPermission } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
type Props<T extends AbilityTuple> = { type Props<T extends AbilityTuple> = {
className?: string;
containerClassName?: string; containerClassName?: string;
action: T[0]; action: T[0];
subject: T[1]; subject: T[1];
@@ -16,7 +14,7 @@ type Props<T extends AbilityTuple> = {
export const withProjectPermission = <T extends object>( export const withProjectPermission = <T extends object>(
Component: ComponentType<Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T>, Component: ComponentType<Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T>,
{ action, subject, className, containerClassName }: Props<ProjectPermissionSet> { action, subject, containerClassName }: Props<ProjectPermissionSet>
) => { ) => {
const HOC = (hocProps: Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T) => { const HOC = (hocProps: Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T) => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
@@ -31,23 +29,7 @@ export const withProjectPermission = <T extends object>(
containerClassName containerClassName
)} )}
> >
<div <AccessRestrictedBanner />
className={twMerge(
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Permission Denied</div>
<div className="text-sm">
You do not have permission to this page. <br /> Kindly contact your organization
administrator
</div>
</div>
</div>
</div> </div>
); );
} }
@@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => {
return useMutation<object, object, TCreateAccessPolicyDTO>({ return useMutation<object, object, TCreateAccessPolicyDTO>({
mutationFn: async ({ mutationFn: async ({
environment, environments,
projectSlug, projectSlug,
approvals, approvals,
approvers, approvers,
@@ -29,7 +29,7 @@ export const useCreateAccessApprovalPolicy = () => {
approvalsRequired approvalsRequired
}) => { }) => {
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
environment, environments,
projectSlug, projectSlug,
approvals, approvals,
bypassers, bypassers,
@@ -63,7 +63,8 @@ export const useUpdateAccessApprovalPolicy = () => {
secretPath, secretPath,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}) => { }) => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals, approvals,
@@ -73,7 +74,8 @@ export const useUpdateAccessApprovalPolicy = () => {
name, name,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}); });
return data; return data;
}, },
@@ -8,9 +8,8 @@ export type TAccessApprovalPolicy = {
name: string; name: string;
approvals: number; approvals: number;
secretPath: string; secretPath: string;
envId: string;
workspace: string; workspace: string;
environment: WorkspaceEnv; environments: WorkspaceEnv[];
projectId: string; projectId: string;
policyType: PolicyType; policyType: PolicyType;
approversRequired: boolean; approversRequired: boolean;
@@ -166,7 +165,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
export type TCreateAccessPolicyDTO = { export type TCreateAccessPolicyDTO = {
projectSlug: string; projectSlug: string;
name?: string; name?: string;
environment: string; environments: string[];
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
approvals?: number; approvals?: number;
@@ -182,7 +181,7 @@ export type TUpdateAccessPolicyDTO = {
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
secretPath?: string; secretPath?: string;
environment?: string; environments?: string[];
approvals?: number; approvals?: number;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
@@ -2,15 +2,26 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum AzureClientSecretsConnectionMethod { export enum AzureClientSecretsConnectionMethod {
OAuth = "oauth" OAuth = "oauth",
ClientSecret = "client-secret"
} }
export type TAzureClientSecretsConnection = TRootAppConnection & { export type TAzureClientSecretsConnection = TRootAppConnection & {
app: AppConnection.AzureClientSecrets; app: AppConnection.AzureClientSecrets;
} & { } & (
method: AzureClientSecretsConnectionMethod.OAuth; | {
credentials: { method: AzureClientSecretsConnectionMethod.OAuth;
code: string; credentials: {
tenantId: string; code: string;
}; tenantId: string;
}; };
}
| {
method: AzureClientSecretsConnectionMethod.ClientSecret;
credentials: {
clientSecret: string;
clientId: string;
tenantId: string;
};
}
);
@@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => {
return useMutation<object, object, TCreateSecretPolicyDTO>({ return useMutation<object, object, TCreateSecretPolicyDTO>({
mutationFn: async ({ mutationFn: async ({
environment, environments,
workspaceId, workspaceId,
approvals, approvals,
approvers, approvers,
@@ -21,7 +21,7 @@ export const useCreateSecretApprovalPolicy = () => {
allowedSelfApprovals allowedSelfApprovals
}) => { }) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", { const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment, environments,
workspaceId, workspaceId,
approvals, approvals,
approvers, approvers,
@@ -53,7 +53,8 @@ export const useUpdateSecretApprovalPolicy = () => {
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals,
environments
}) => { }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals, approvals,
@@ -62,7 +63,8 @@ export const useUpdateSecretApprovalPolicy = () => {
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals,
environments
}); });
return data; return data;
}, },
@@ -5,8 +5,7 @@ export type TSecretApprovalPolicy = {
id: string; id: string;
workspace: string; workspace: string;
name: string; name: string;
envId: string; environments: WorkspaceEnv[];
environment: WorkspaceEnv;
secretPath?: string; secretPath?: string;
approvals: number; approvals: number;
approvers: Approver[]; approvers: Approver[];
@@ -48,7 +47,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
export type TCreateSecretPolicyDTO = { export type TCreateSecretPolicyDTO = {
workspaceId: string; workspaceId: string;
name?: string; name?: string;
environment: string; environments: string[];
secretPath: string; secretPath: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
@@ -68,6 +67,7 @@ export type TUpdateSecretPolicyDTO = {
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
// for invalidating list // for invalidating list
workspaceId: string; workspaceId: string;
environments?: string[];
}; };
export type TDeleteSecretPolicyDTO = { export type TDeleteSecretPolicyDTO = {
@@ -1,7 +1,6 @@
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs"; import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
import { DiscriminativePick } from "@app/types"; import { DiscriminativePick } from "@app/types";
import { TRenderSync } from "../render-sync";
import { TOnePassSync } from "./1password-sync"; import { TOnePassSync } from "./1password-sync";
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync"; import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
@@ -24,6 +23,7 @@ import { THerokuSync } from "./heroku-sync";
import { THumanitecSync } from "./humanitec-sync"; import { THumanitecSync } from "./humanitec-sync";
import { TOCIVaultSync } from "./oci-vault-sync"; import { TOCIVaultSync } from "./oci-vault-sync";
import { TRailwaySync } from "./railway-sync"; import { TRailwaySync } from "./railway-sync";
import { TRenderSync } from "./render-sync";
import { TSupabaseSync } from "./supabase"; import { TSupabaseSync } from "./supabase";
import { TTeamCitySync } from "./teamcity-sync"; import { TTeamCitySync } from "./teamcity-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync";
@@ -1,6 +1,6 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TRenderSync = TRootSecretSync & { export type TRenderSync = TRootSecretSync & {
destination: SecretSync.Render; destination: SecretSync.Render;
@@ -16,6 +16,10 @@ export type TRenderSync = TRootSecretSync & {
name: string; name: string;
id: string; id: string;
}; };
syncOptions: RootSyncOptions & {
autoRedeployServices?: boolean;
};
}; };
export enum RenderSyncScope { export enum RenderSyncScope {
+2 -1
View File
@@ -49,7 +49,8 @@ export const usePathAccessPolicies = ({ secretPath, environment }: Params) => {
return useMemo(() => { return useMemo(() => {
const pathPolicies = policies?.filter( const pathPolicies = policies?.filter(
(policy) => (policy) =>
policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath) policy.environments?.some((env) => env.slug === environment) &&
matchesPath(secretPath, policy.secretPath)
); );
return { return {
@@ -22,7 +22,6 @@ export const KmipPage = () => {
description="Integrate with Infisical KMS via Key Management Interoperability Protocol." description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
/> />
<ProjectPermissionCan <ProjectPermissionCan
passThrough={false}
renderGuardBanner renderGuardBanner
I={ProjectPermissionKmipActions.ReadClients} I={ProjectPermissionKmipActions.ReadClients}
a={ProjectPermissionSub.Kmip} a={ProjectPermissionSub.Kmip}
@@ -22,7 +22,6 @@ export const OverviewPage = () => {
description="Manage keys and perform cryptographic operations." description="Manage keys and perform cryptographic operations."
/> />
<ProjectPermissionCan <ProjectPermissionCan
passThrough={false}
renderGuardBanner renderGuardBanner
I={ProjectPermissionActions.Read} I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Cmek} a={ProjectPermissionSub.Cmek}
@@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Camunda: case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />; return <CamundaConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureClientSecrets: case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm />; return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureDevOps: case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />; return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
case AppConnection.Windmill: case AppConnection.Windmill:
@@ -222,7 +222,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Camunda: case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.AzureClientSecrets: case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm appConnection={appConnection} />; return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureDevOps: case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />; return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.Windmill: case AppConnection.Windmill:
@@ -1,3 +1,4 @@
/* eslint-disable no-case-declarations */
import crypto from "crypto"; import crypto from "crypto";
import { useState } from "react"; import { useState } from "react";
@@ -20,19 +21,83 @@ import {
GenericAppConnectionsFields GenericAppConnectionsFields
} from "./GenericAppConnectionFields"; } from "./GenericAppConnectionFields";
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = { type Props = {
appConnection?: TAzureClientSecretsConnection; appConnection?: TAzureClientSecretsConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
}; };
const formSchema = genericAppConnectionFieldsSchema.extend({ const baseSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureClientSecrets), app: z.literal(AppConnection.AzureClientSecrets),
method: z.nativeEnum(AzureClientSecretsConnectionMethod), method: z.nativeEnum(AzureClientSecretsConnectionMethod)
tenantId: z.string().trim().min(1, "Tenant ID is required")
}); });
const oauthSchema = baseSchema.extend({
tenantId: z.string().trim().min(1, "Tenant ID is required"),
method: z.literal(AzureClientSecretsConnectionMethod.OAuth)
});
const clientSecretSchema = baseSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
credentials: z.object({
clientSecret: z.string().trim().min(1, "Client Secret is required"),
clientId: z.string().trim().min(1, "Client ID is required"),
tenantId: z.string().trim().min(1, "Tenant ID is required")
})
});
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => { const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partial<FormData> => {
if (!appConnection) {
return {
app: AppConnection.AzureClientSecrets,
method: AzureClientSecretsConnectionMethod.OAuth
};
}
const base = {
name: appConnection.name,
description: appConnection.description,
app: appConnection.app,
method: appConnection.method
};
const { credentials } = appConnection;
switch (appConnection.method) {
case AzureClientSecretsConnectionMethod.OAuth:
if ("tenantId" in credentials) {
return {
...base,
method: AzureClientSecretsConnectionMethod.OAuth,
tenantId: credentials.tenantId
};
}
break;
case AzureClientSecretsConnectionMethod.ClientSecret:
if ("clientSecret" in credentials && "clientId" in credentials) {
return {
...base,
method: AzureClientSecretsConnectionMethod.ClientSecret,
credentials: {
clientSecret: credentials.clientSecret,
clientId: credentials.clientId,
tenantId: credentials.tenantId
}
};
}
break;
default:
return base;
}
return base;
};
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection); const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false); const [isRedirecting, setIsRedirecting] = useState(false);
@@ -43,70 +108,51 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
const form = useForm<FormData>({ const form = useForm<FormData>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: appConnection defaultValues: getDefaultValues(appConnection)
? {
...appConnection,
tenantId: appConnection.credentials.tenantId
}
: {
app: AppConnection.AzureClientSecrets,
method: AzureClientSecretsConnectionMethod.OAuth
}
}); });
const { const {
handleSubmit, handleSubmit,
control, control,
watch, watch,
setValue,
formState: { isSubmitting, isDirty } formState: { isSubmitting, isDirty }
} = form; } = form;
const selectedMethod = watch("method"); const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => { const onSubmitHandler = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex"); const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) { switch (formData.method) {
case AzureClientSecretsConnectionMethod.OAuth: case AzureClientSecretsConnectionMethod.OAuth:
window.location.assign( setIsRedirecting(true);
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets` localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
); );
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
);
break;
case AzureClientSecretsConnectionMethod.ClientSecret:
onSubmit(formData);
break; break;
default: default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`); throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
} }
}; };
const isMissingConfig = !oauthClientId; const isMissingConfig =
selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId;
const methodDetails = getAppConnectionMethodDetails(selectedMethod); const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmitHandler)}>
{!isUpdate && <GenericAppConnectionsFields />} {!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Directory (tenant) ID."
isError={Boolean(error?.message)}
label="Tenant ID"
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
</FormControl>
)}
/>
<Controller <Controller
name="method" name="method"
control={control} control={control}
@@ -146,6 +192,61 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Directory (tenant) ID."
isError={Boolean(error?.message)}
label="Tenant ID"
errorText={error?.message}
>
<Input
{...field}
placeholder="00000000-0000-0000-0000-000000000000"
onChange={(e) => {
field.onChange(e.target.value);
setValue("credentials.tenantId", e.target.value);
}}
/>
</FormControl>
)}
/>
{/* Access Token-specific fields */}
{selectedMethod === AzureClientSecretsConnectionMethod.ClientSecret && (
<>
<Controller
name="credentials.clientId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client ID"
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
</FormControl>
)}
/>
<Controller
name="credentials.clientSecret"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client Secret"
errorText={error?.message}
>
<Input {...field} type="password" placeholder="Enter your Client Secret" />
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -155,8 +155,8 @@ export const SpecificPrivilegeSecretForm = ({
const selectablePaths = useMemo(() => { const selectablePaths = useMemo(() => {
if (!policies) return []; if (!policies) return [];
const environmentPolicies = policies.filter( const environmentPolicies = policies.filter((policy) =>
(policy) => policy.environment.slug === selectedEnvironment policy.environments.find((env) => env.slug === selectedEnvironment)
); );
privilegeForm.setValue("secretPath", "", { privilegeForm.setValue("secretPath", "", {
@@ -1,5 +1,5 @@
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render"; import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import { getSecretSyncDestinationColValues } from "../helpers"; import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell"; import { SecretSyncTableCell } from "../SecretSyncTableCell";
@@ -166,17 +166,20 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
const filteredPolicies = useMemo( const filteredPolicies = useMemo(
() => () =>
policies policies
.filter(({ policyType, environment, name, secretPath }) => { .filter(({ policyType, environments, name, secretPath }) => {
if (filters.type && policyType !== filters.type) return false; if (filters.type && policyType !== filters.type) return false;
if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id)) if (
filters.environmentIds.length &&
!environments.some((env) => filters.environmentIds.includes(env.id))
)
return false; return false;
const searchValue = search.trim().toLowerCase(); const searchValue = search.trim().toLowerCase();
return ( return (
name.toLowerCase().includes(searchValue) || name.toLowerCase().includes(searchValue) ||
environment.name.toLowerCase().includes(searchValue) || environments.some((env) => env.name.toLowerCase().includes(searchValue)) ||
(secretPath ?? "*").toLowerCase().includes(searchValue) (secretPath ?? "*").toLowerCase().includes(searchValue)
); );
}) })
@@ -189,9 +192,18 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
.toLowerCase() .toLowerCase()
.localeCompare(policyTwo.policyType.toLowerCase()); .localeCompare(policyTwo.policyType.toLowerCase());
case PolicyOrderBy.Environment: case PolicyOrderBy.Environment:
return policyOne.environment.name // eslint-disable-next-line no-case-declarations
.toLowerCase() const getFirstEnvName = (policy: { environments: { name: string }[] }) => {
.localeCompare(policyTwo.environment.name.toLowerCase()); if (!policy.environments?.length) return "";
return (
policy.environments
.map((env) => env.name?.toLowerCase() || "")
.filter((name) => name)
.sort()[0] || ""
);
};
return getFirstEnvName(policyOne).localeCompare(getFirstEnvName(policyTwo));
case PolicyOrderBy.SecretPath: case PolicyOrderBy.SecretPath:
return (policyOne.secretPath ?? "*") return (policyOne.secretPath ?? "*")
.toLowerCase() .toLowerCase()
@@ -54,7 +54,7 @@ type Props = {
const formSchema = z const formSchema = z
.object({ .object({
environment: z.object({ slug: z.string(), name: z.string() }), environments: z.array(z.object({ slug: z.string(), name: z.string() })).min(1),
name: z.string().optional(), name: z.string().optional(),
secretPath: z.string().trim().min(1), secretPath: z.string().trim().min(1),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
@@ -134,7 +134,7 @@ const Form = ({
values: editValues values: editValues
? ({ ? ({
...editValues, ...editValues,
environment: editValues.environment, environments: editValues.environments,
userApprovers: userApprovers:
editValues?.approvers editValues?.approvers
?.filter((approver) => approver.type === ApproverType.User) ?.filter((approver) => approver.type === ApproverType.User)
@@ -191,7 +191,7 @@ const Form = ({
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: groups } = useListWorkspaceGroups(projectId); const { data: groups } = useListWorkspaceGroups(projectId);
const environments = currentWorkspace?.environments || []; const availableEnvironments = currentWorkspace?.environments || [];
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy; const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
@@ -204,11 +204,11 @@ const Form = ({
const formUserBypassers = watch("userBypassers"); const formUserBypassers = watch("userBypassers");
const formGroupBypassers = watch("groupBypassers"); const formGroupBypassers = watch("groupBypassers");
const formEnvironment = watch("environment")?.slug; const formEnvironments = watch("environments");
const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length; const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length;
const handleCreatePolicy = async ({ const handleCreatePolicy = async ({
environment, environments,
groupApprovers, groupApprovers,
userApprovers, userApprovers,
groupBypassers, groupBypassers,
@@ -226,7 +226,7 @@ const Form = ({
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environments: environments.map((env) => env.slug),
workspaceId: currentWorkspace?.id || "" workspaceId: currentWorkspace?.id || ""
}); });
} else { } else {
@@ -242,7 +242,7 @@ const Form = ({
numberOfApprovals: el.approvals numberOfApprovals: el.approvals
})), })),
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environments: environments.map((env) => env.slug),
projectSlug projectSlug
}); });
} }
@@ -261,7 +261,7 @@ const Form = ({
}; };
const handleUpdatePolicy = async ({ const handleUpdatePolicy = async ({
environment, environments,
userApprovers, userApprovers,
groupApprovers, groupApprovers,
userBypassers, userBypassers,
@@ -281,7 +281,8 @@ const Form = ({
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
workspaceId: currentWorkspace?.id || "" workspaceId: currentWorkspace?.id || "",
environments: environments.map((env) => env.slug)
}); });
} else { } else {
await updateAccessApprovalPolicy({ await updateAccessApprovalPolicy({
@@ -297,7 +298,7 @@ const Form = ({
numberOfApprovals: el.approvals numberOfApprovals: el.approvals
})), })),
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environments: environments.map((env) => env.slug),
projectSlug projectSlug
}); });
} }
@@ -479,28 +480,28 @@ const Form = ({
<SecretPathInput <SecretPathInput
{...field} {...field}
value={field.value || ""} value={field.value || ""}
environment={formEnvironment} environment={formEnvironments?.[0]?.slug || ""}
/> />
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="environment" name="environments"
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
label="Environment" label="Environments"
isRequired isRequired
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="flex-1" className="flex-1"
> >
<FilterableSelect <FilterableSelect
isDisabled={isEditMode}
value={value} value={value}
isMulti
onChange={onChange} onChange={onChange}
placeholder="Select environment..." placeholder="Select environments..."
options={environments} options={availableEnvironments}
getOptionValue={(option) => option.slug} getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
/> />
@@ -37,7 +37,7 @@ import { TWorkspaceUser } from "@app/hooks/api/users/types";
interface IPolicy { interface IPolicy {
id: string; id: string;
name: string; name: string;
environment: WorkspaceEnv; environments: WorkspaceEnv[];
projectId?: string; projectId?: string;
secretPath?: string; secretPath?: string;
approvals: number; approvals: number;
@@ -112,7 +112,7 @@ export const ApprovalPolicyRow = ({
onClick={() => setIsExpanded.toggle()} onClick={() => setIsExpanded.toggle()}
> >
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td> <Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td>
<Td>{policy.environment.name}</Td> <Td>{policy.environments.map((env) => env.name).join(", ")}</Td>
<Td>{policy.secretPath || "*"}</Td> <Td>{policy.secretPath || "*"}</Td>
<Td> <Td>
<Badge <Badge
@@ -975,12 +975,12 @@ const Page = () => {
/> />
)} )}
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />} {noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
{!canReadSecret &&
!canReadDynamicSecret &&
!canReadSecretImports &&
folders?.length === 0 && <PermissionDeniedBanner />}
</div> </div>
</div> </div>
{!canReadSecret &&
!canReadDynamicSecret &&
!canReadSecretImports &&
folders?.length === 0 && <PermissionDeniedBanner />}
{!isDetailsLoading && {!isDetailsLoading &&
(totalCount > 0 || (totalCount > 0 ||
pendingChanges.secrets.length > 0 || pendingChanges.secrets.length > 0 ||
@@ -1,6 +1,6 @@
import { GenericFieldLabel } from "@app/components/secret-syncs"; import { GenericFieldLabel } from "@app/components/secret-syncs";
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render"; import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
type Props = { type Props = {
secretSync: TRenderSync; secretSync: TRenderSync;
@@ -0,0 +1,21 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { Badge } from "@app/components/v2";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
type Props = {
secretSync: TRenderSync;
};
export const RenderSyncOptionsSection = ({ secretSync }: Props) => {
const {
syncOptions: { autoRedeployServices }
} = secretSync;
return (
<GenericFieldLabel label="Auto Redeploy Services">
<Badge variant={autoRedeployServices ? "success" : "danger"}>
{autoRedeployServices ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
);
};
@@ -13,6 +13,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection"; import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection"; import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
import { RenderSyncOptionsSection } from "./RenderSyncOptionsSection";
type Props = { type Props = {
secretSync: TSecretSync; secretSync: TSecretSync;
@@ -40,6 +41,9 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} /> <AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
); );
break; break;
case SecretSync.Render:
AdditionalSyncOptionsComponent = <RenderSyncOptionsSection secretSync={secretSync} />;
break;
case SecretSync.GitHub: case SecretSync.GitHub:
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
@@ -56,7 +60,6 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.OCIVault: case SecretSync.OCIVault:
case SecretSync.OnePass: case SecretSync.OnePass:
case SecretSync.Heroku: case SecretSync.Heroku:
case SecretSync.Render:
case SecretSync.Flyio: case SecretSync.Flyio:
case SecretSync.GitLab: case SecretSync.GitLab:
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
@@ -1,6 +1,6 @@
import { faBan } from "@fortawesome/free-solid-svg-icons"; import { faBan } from "@fortawesome/free-solid-svg-icons";
import { ContentLoader, EmptyState } from "@app/components/v2"; import { AccessRestrictedBanner, ContentLoader, EmptyState } from "@app/components/v2";
import { useSubscription, useWorkspace } from "@app/context"; import { useSubscription, useWorkspace } from "@app/context";
import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2"; import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2";
@@ -16,16 +16,14 @@ export const ProjectScanningConfigTab = () => {
if (!subscription.secretScanning) { if (!subscription.secretScanning) {
return ( return (
<div className="flex h-full w-full items-center justify-center px-20"> <div className="mt-60 flex h-full w-full items-center justify-center px-20">
<EmptyState <AccessRestrictedBanner
className="rounded-md text-center" body={
icon={faBan} <>
title={
<span>
Your current plan doesn&apos;t support Secret Scanning. Your current plan doesn&apos;t support Secret Scanning.
<br /> Please contact Infisical Support or reach out through our Slack channel for <br /> Please contact Infisical Support or reach out through our Slack channel for
assistance. assistance.
</span> </>
} }
/> />
</div> </div>
+9 -8
View File
@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.8.3", "axios": "^1.11.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"form-data": "^4.0.2", "form-data": "^4.0.2",
@@ -105,13 +105,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.3", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -571,14 +571,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
+1 -1
View File
@@ -11,7 +11,7 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"axios": "^1.8.3", "axios": "^1.11.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"form-data": "^4.0.2", "form-data": "^4.0.2",