diff --git a/.env.example b/.env.example
index 05a888db0..059ec124f 100644
--- a/.env.example
+++ b/.env.example
@@ -123,8 +123,17 @@ INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET=
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
# azure app connection
-INF_APP_CONNECTION_AZURE_CLIENT_ID=
-INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
+INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID=
+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
SHOULD_USE_DATADOG_TRACER=
diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical
index d2b2a2d87..b95794832 100644
--- a/Dockerfile.fips.standalone-infisical
+++ b/Dockerfile.fips.standalone-infisical
@@ -145,7 +145,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \
&& ./Configure enable-fips \
&& 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
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_MODE true
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:
ENV OPENSSL_CONF=/backend/nodejs.fips.cnf
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
-ENV NODE_OPTIONS=--force-fips
ENV FIPS_ENABLED=true
diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips
index 977362e03..b954ccd50 100644
--- a/backend/Dockerfile.dev.fips
+++ b/backend/Dockerfile.dev.fips
@@ -59,7 +59,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \
&& ./Configure enable-fips \
&& 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
diff --git a/backend/package-lock.json b/backend/package-lock.json
index a5c106540..cb9efa148 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -7,6 +7,7 @@
"": {
"name": "backend",
"version": "1.0.0",
+ "hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@aws-sdk/client-elasticache": "^3.637.0",
@@ -61,7 +62,7 @@
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1553.0",
- "axios": "^1.6.7",
+ "axios": "^1.11.0",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
@@ -13699,14 +13700,16 @@
}
},
"node_modules/@types/request/node_modules/form-data": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz",
- "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
+ "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"engines": {
@@ -15230,13 +15233,13 @@
}
},
"node_modules/axios": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
- "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -18761,13 +18764,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
- "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
diff --git a/backend/package.json b/backend/package.json
index bd355dd22..01bb0c42a 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -181,7 +181,7 @@
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1553.0",
- "axios": "^1.6.7",
+ "axios": "^1.11.0",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts
index 7fac20c0e..185a32356 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -489,6 +489,11 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
+import {
+ TAccessApprovalPoliciesEnvironments,
+ TAccessApprovalPoliciesEnvironmentsInsert,
+ TAccessApprovalPoliciesEnvironmentsUpdate
+} from "@app/db/schemas/access-approval-policies-environments";
import {
TIdentityLdapAuths,
TIdentityLdapAuthsInsert,
@@ -510,6 +515,11 @@ import {
TRemindersRecipientsInsert,
TRemindersRecipientsUpdate
} from "@app/db/schemas/reminders-recipients";
+import {
+ TSecretApprovalPoliciesEnvironments,
+ TSecretApprovalPoliciesEnvironmentsInsert,
+ TSecretApprovalPoliciesEnvironmentsUpdate
+} from "@app/db/schemas/secret-approval-policies-environments";
import {
TSecretReminderRecipients,
TSecretReminderRecipientsInsert,
@@ -887,6 +897,12 @@ declare module "knex/types/tables" {
TAccessApprovalPoliciesBypassersUpdate
>;
+ [TableName.AccessApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
+ TAccessApprovalPoliciesEnvironments,
+ TAccessApprovalPoliciesEnvironmentsInsert,
+ TAccessApprovalPoliciesEnvironmentsUpdate
+ >;
+
[TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType<
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
@@ -935,6 +951,11 @@ declare module "knex/types/tables" {
TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate
>;
+ [TableName.SecretApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
+ TSecretApprovalPoliciesEnvironments,
+ TSecretApprovalPoliciesEnvironmentsInsert,
+ TSecretApprovalPoliciesEnvironmentsUpdate
+ >;
[TableName.SecretRotation]: KnexOriginal.CompositeTableType<
TSecretRotations,
TSecretRotationsInsert,
diff --git a/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts
new file mode 100644
index 000000000..57ec13203
--- /dev/null
+++ b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts
@@ -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 {
+ 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 {
+ 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");
+ });
+}
diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts
new file mode 100644
index 000000000..b720c97ae
--- /dev/null
+++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts
@@ -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 {
+ 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 {
+ logger.info("Rollback not implemented for secret reminders fix migration");
+}
diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts
index debaea03f..de32f4db9 100644
--- a/backend/src/db/migrations/utils/env-config.ts
+++ b/backend/src/db/migrations/utils/env-config.ts
@@ -53,7 +53,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory
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:
// 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.
diff --git a/backend/src/db/schemas/access-approval-policies-environments.ts b/backend/src/db/schemas/access-approval-policies-environments.ts
new file mode 100644
index 000000000..fa2a859c2
--- /dev/null
+++ b/backend/src/db/schemas/access-approval-policies-environments.ts
@@ -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;
+export type TAccessApprovalPoliciesEnvironmentsInsert = Omit<
+ z.input,
+ TImmutableDBKeys
+>;
+export type TAccessApprovalPoliciesEnvironmentsUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index 2e71486bf..55ec12faa 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -100,6 +100,7 @@ export enum TableName {
AccessApprovalPolicyBypasser = "access_approval_policies_bypassers",
AccessApprovalRequest = "access_approval_requests",
AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
+ AccessApprovalPolicyEnvironment = "access_approval_policies_environments",
SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers",
@@ -107,6 +108,7 @@ export enum TableName {
SecretApprovalRequestReviewer = "secret_approval_requests_reviewers",
SecretApprovalRequestSecret = "secret_approval_requests_secrets",
SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags",
+ SecretApprovalPolicyEnvironment = "secret_approval_policies_environments",
SecretRotation = "secret_rotations",
SecretRotationOutput = "secret_rotation_outputs",
SamlConfig = "saml_configs",
diff --git a/backend/src/db/schemas/secret-approval-policies-environments.ts b/backend/src/db/schemas/secret-approval-policies-environments.ts
new file mode 100644
index 000000000..0420fe75d
--- /dev/null
+++ b/backend/src/db/schemas/secret-approval-policies-environments.ts
@@ -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;
+export type TSecretApprovalPoliciesEnvironmentsInsert = Omit<
+ z.input,
+ TImmutableDBKeys
+>;
+export type TSecretApprovalPoliciesEnvironmentsUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts
index 177f5e1fd..ef44344de 100644
--- a/backend/src/ee/routes/v1/access-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts
@@ -17,52 +17,66 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit
},
schema: {
- body: z.object({
- projectSlug: z.string().trim(),
- name: z.string().optional(),
- secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash),
- environment: z.string(),
- approvers: z
- .discriminatedUnion("type", [
- z.object({
- type: z.literal(ApproverType.Group),
- id: z.string(),
- 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)
+ body: z
+ .object({
+ projectSlug: z.string().trim(),
+ name: z.string().optional(),
+ secretPath: z
+ .string()
+ .trim()
+ .min(1, { message: "Secret path cannot be empty" })
+ .transform(removeTrailingSlash),
+ environment: z.string().optional(),
+ environments: z.string().array().optional(),
+ approvers: z
+ .discriminatedUnion("type", [
+ z.object({
+ type: z.literal(ApproverType.Group),
+ id: z.string(),
+ 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()
- .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()
- .optional(),
- approvals: z.number().min(1).default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
- allowedSelfApprovals: z.boolean().default(true)
- }),
+ .array()
+ .optional(),
+ approvals: z.number().min(1).default(1),
+ enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
+ allowedSelfApprovals: z.boolean().default(true)
+ })
+ .refine(
+ (val) => Boolean(val.environment) || Boolean(val.environments),
+ "Must provide either environment or environments"
+ ),
response: {
200: z.object({
approval: sapPubSchema
@@ -78,7 +92,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
...req.body,
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
});
return { approval };
@@ -211,6 +226,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: z.number().min(1).optional(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true),
+ environments: z.array(z.string()).optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
index 46b2544b2..dc87b83f2 100644
--- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
@@ -17,34 +17,45 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit
},
schema: {
- body: z.object({
- workspaceId: z.string(),
- name: z.string().optional(),
- environment: z.string(),
- secretPath: z
- .string()
- .min(1, { message: "Secret path cannot be empty" })
- .transform((val) => removeTrailingSlash(val)),
- approvers: z
- .discriminatedUnion("type", [
- z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
- z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
- ])
- .array()
- .min(1, { message: "At least one approver should be provided" })
- .max(100, "Cannot have more than 100 approvers"),
- 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(),
- approvals: z.number().min(1).default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
- allowedSelfApprovals: z.boolean().default(true)
- }),
+ body: z
+ .object({
+ workspaceId: z.string(),
+ name: z.string().optional(),
+ environment: z.string().optional(),
+ environments: z.string().array().optional(),
+ secretPath: z
+ .string()
+ .min(1, { message: "Secret path cannot be empty" })
+ .transform((val) => removeTrailingSlash(val)),
+ approvers: z
+ .discriminatedUnion("type", [
+ z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
+ z.object({
+ type: z.literal(ApproverType.User),
+ id: z.string().optional(),
+ username: z.string().optional()
+ })
+ ])
+ .array()
+ .min(1, { message: "At least one approver should be provided" })
+ .max(100, "Cannot have more than 100 approvers"),
+ 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(),
+ 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: {
200: z.object({
approval: sapPubSchema
@@ -60,7 +71,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...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
});
return { approval };
@@ -103,7 +114,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.optional()
.transform((val) => (val ? removeTrailingSlash(val) : undefined)),
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
- allowedSelfApprovals: z.boolean().default(true)
+ allowedSelfApprovals: z.boolean().default(true),
+ environments: z.array(z.string()).optional()
}),
response: {
200: z.object({
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts
index 995534f8f..9baf762d6 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts
@@ -26,6 +26,7 @@ export interface TAccessApprovalPolicyDALFactory
>,
customFilter?: {
policyId?: string;
+ envId?: string;
},
tx?: Knex
) => Promise<
@@ -55,11 +56,6 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
- environment: {
- id: string;
- name: string;
- slug: string;
- };
projectId: string;
bypassers: (
| {
@@ -72,6 +68,11 @@ export interface TAccessApprovalPolicyDALFactory
type: BypasserType.Group;
}
)[];
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
}[]
>;
findById: (
@@ -95,11 +96,11 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
- environment: {
+ environments: {
id: string;
name: string;
slug: string;
- };
+ }[];
projectId: string;
}
| undefined
@@ -143,6 +144,26 @@ export interface TAccessApprovalPolicyDALFactory
}
| 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 {
@@ -367,6 +388,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
filter: TFindFilter,
customFilter?: {
policyId?: string;
+ envId?: string;
}
) => {
const result = await tx(TableName.AccessApprovalPolicy)
@@ -377,7 +399,17 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
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(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
@@ -404,7 +436,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
- .select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
+ .select(tx.ref("id").withSchema(TableName.Environment).as("environmentId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
@@ -448,6 +480,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
sequence: approverSequence,
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,
key: "id",
parentMapper: (data) => ({
- environment: {
- id: data.envId,
- name: data.envName,
- slug: data.envSlug
- },
projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data)
// secretPath: data.secretPath || undefined,
@@ -517,6 +553,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
id,
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
buildFindFilter(
{
- envId,
secretPath
},
TableName.AccessApprovalPolicy
)
)
+ .join(
+ TableName.AccessApprovalPolicyEnvironment,
+ `${TableName.AccessApprovalPolicyEnvironment}.policyId`,
+ `${TableName.AccessApprovalPolicy}.id`
+ )
+ .where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", envId)
.orderBy("deletedAt", "desc")
.orderByRaw(`"deletedAt" IS NULL`)
+ .select(selectAllTableCols(TableName.AccessApprovalPolicy))
.first();
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
+ };
};
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts
new file mode 100644
index 000000000..f0d8079cf
--- /dev/null
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts
@@ -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;
+
+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 };
+};
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
index 6d656bafa..0b3c4e128 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
@@ -21,6 +21,7 @@ import {
TAccessApprovalPolicyBypasserDALFactory
} from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
+import { TAccessApprovalPolicyEnvironmentDALFactory } from "./access-approval-policy-environment-dal";
import {
ApproverType,
BypasserType,
@@ -45,12 +46,14 @@ type TAccessApprovalPolicyServiceFactoryDep = {
additionalPrivilegeDAL: Pick;
accessApprovalRequestReviewerDAL: Pick;
orgMembershipDAL: Pick;
+ accessApprovalPolicyEnvironmentDAL: TAccessApprovalPolicyEnvironmentDALFactory;
};
export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL,
+ accessApprovalPolicyEnvironmentDAL,
groupDAL,
permissionService,
projectEnvDAL,
@@ -63,21 +66,22 @@ export const accessApprovalPolicyServiceFactory = ({
}: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => {
const $policyExists = async ({
envId,
+ envIds,
secretPath,
policyId
}: {
- envId: string;
+ envId?: string;
+ envIds?: string[];
secretPath: string;
policyId?: string;
}) => {
- const policy = await accessApprovalPolicyDAL
- .findOne({
- envId,
- secretPath,
- deletedAt: null
- })
- .catch(() => null);
-
+ if (!envId && !envIds) {
+ throw new BadRequestError({ message: "Must provide either envId or envIds" });
+ }
+ const policy = await accessApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({
+ secretPath,
+ envIds: envId ? [envId] : (envIds as string[])
+ });
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
@@ -93,6 +97,7 @@ export const accessApprovalPolicyServiceFactory = ({
bypassers,
projectSlug,
environment,
+ environments,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
@@ -125,13 +130,23 @@ export const accessApprovalPolicyServiceFactory = ({
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
);
- const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
- if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` });
+ const mergedEnvs = (environment ? [environment] : environments) || [];
+ 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 })) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
- });
+ for (const env of envs) {
+ // eslint-disable-next-line no-await-in-loop
+ 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;
@@ -199,7 +214,7 @@ export const accessApprovalPolicyServiceFactory = ({
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create(
{
- envId: env.id,
+ envId: envs[0].id,
approvals,
secretPath,
name,
@@ -208,6 +223,10 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
+ await accessApprovalPolicyEnvironmentDAL.insertMany(
+ envs.map((el) => ({ policyId: doc.id, envId: el.id })),
+ tx
+ );
if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany(
@@ -260,7 +279,7 @@ export const accessApprovalPolicyServiceFactory = ({
return doc;
});
- return { ...accessApproval, environment: env, projectId: project.id };
+ return { ...accessApproval, environments: envs, projectId: project.id, environment: envs[0] };
};
const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] =
@@ -279,7 +298,10 @@ export const accessApprovalPolicyServiceFactory = ({
});
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 ({
@@ -295,7 +317,8 @@ export const accessApprovalPolicyServiceFactory = ({
approvals,
enforcementLevel,
allowedSelfApprovals,
- approvalsRequired
+ approvalsRequired,
+ environments
}: TUpdateAccessApprovalPolicy) => {
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" });
}
+ let envs = accessApprovalPolicy.environments;
if (
- await $policyExists({
- envId: accessApprovalPolicy.envId,
- secretPath: secretPath || accessApprovalPolicy.secretPath,
- policyId: accessApprovalPolicy.id
- })
+ environments &&
+ (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'`
- });
+ envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: accessApprovalPolicy.projectId });
+ }
+
+ 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({
@@ -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);
if (bypasserUserIds.length) {
@@ -517,7 +559,8 @@ export const accessApprovalPolicyServiceFactory = ({
return {
...updatedPolicy,
- environment: accessApprovalPolicy.environment,
+ environments: accessApprovalPolicy.environments,
+ environment: accessApprovalPolicy.environments[0],
projectId: accessApprovalPolicy.projectId
};
};
@@ -568,7 +611,10 @@ export const accessApprovalPolicyServiceFactory = ({
}
});
- return policy;
+ return {
+ ...policy,
+ environment: policy.environments[0]
+ };
};
const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({
@@ -598,11 +644,13 @@ export const accessApprovalPolicyServiceFactory = ({
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
- const policies = await accessApprovalPolicyDAL.find({
- envId: environment.id,
- projectId: project.id,
- deletedAt: null
- });
+ const policies = await accessApprovalPolicyDAL.find(
+ {
+ projectId: project.id,
+ deletedAt: null
+ },
+ { envId: environment.id }
+ );
if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` });
return { count: policies.length };
@@ -634,7 +682,10 @@ export const accessApprovalPolicyServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
- return policy;
+ return {
+ ...policy,
+ environment: policy.environments[0]
+ };
};
return {
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
index f3f195914..27ec228f7 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
@@ -26,7 +26,8 @@ export enum BypasserType {
export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
- environment: string;
+ environment?: string;
+ environments?: string[];
approvers: (
| { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
@@ -58,6 +59,7 @@ export type TUpdateAccessApprovalPolicy = {
enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
+ environments?: string[];
} & Omit;
export type TDeleteAccessApprovalPolicy = {
@@ -113,6 +115,15 @@ export interface TAccessApprovalPolicyServiceFactory {
slug: string;
position: number;
};
+ environments: {
+ name: string;
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ projectId: string;
+ slug: string;
+ position: number;
+ }[];
projectId: string;
name: string;
id: string;
@@ -153,6 +164,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
}>;
updateAccessApprovalPolicy: ({
@@ -168,13 +184,19 @@ export interface TAccessApprovalPolicyServiceFactory {
approvals,
enforcementLevel,
allowedSelfApprovals,
- approvalsRequired
+ approvalsRequired,
+ environments
}: TUpdateAccessApprovalPolicy) => Promise<{
environment: {
id: string;
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
name: string;
id: string;
@@ -225,6 +247,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
bypassers: (
| {
@@ -276,6 +303,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
bypassers: (
| {
diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
index 671d2c1de..9872df067 100644
--- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
+++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
@@ -65,7 +65,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit environment
}
]
});
diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
index bdf579616..dcbe717da 100644
--- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
+++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
@@ -86,6 +86,25 @@ export const accessApprovalRequestServiceFactory = ({
projectMicrosoftTeamsConfigDAL,
projectSlackConfigDAL
}: 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;
+ 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 ({
isTemporary,
temporaryRange,
@@ -308,6 +327,15 @@ export const accessApprovalRequestServiceFactory = ({
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 };
};
@@ -325,13 +353,27 @@ export const accessApprovalRequestServiceFactory = ({
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
}
- const { policy, environment } = accessApprovalRequest;
+ const { policy, environments, permissions } = accessApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
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({
actor,
actorId,
@@ -553,7 +595,7 @@ export const accessApprovalRequestServiceFactory = ({
requesterEmail: actingUser.email,
bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/",
- environment,
+ environment: environment?.name || permissionEnvironment,
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`,
requestType: "access"
},
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts
index fd8be93cf..3212fb902 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts
@@ -23,6 +23,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
filter: TFindFilter,
customFilter?: {
sapId?: string;
+ envId?: string;
}
) =>
tx(TableName.SecretApprovalPolicy)
@@ -33,7 +34,17 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
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(
TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`,
@@ -97,7 +108,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
.select(
tx.ref("name").withSchema(TableName.Environment).as("envName"),
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)
)
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
@@ -146,6 +157,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
firstName,
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,
customFilter?: {
sapId?: string;
+ envId?: string;
},
tx?: Knex
) => {
@@ -221,6 +242,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
mapper: ({ approverGroupUserId: 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 { ...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 };
};
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts
new file mode 100644
index 000000000..d12ace04c
--- /dev/null
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts
@@ -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;
+
+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 };
+};
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
index 41a635e2f..96757dc22 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
@@ -19,6 +19,7 @@ import {
TSecretApprovalPolicyBypasserDALFactory
} from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
+import { TSecretApprovalPolicyEnvironmentDALFactory } from "./secret-approval-policy-environment-dal";
import {
TCreateSapDTO,
TDeleteSapDTO,
@@ -36,12 +37,13 @@ const getPolicyScore = (policy: { secretPath?: string | null }) =>
type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
- projectEnvDAL: Pick;
+ projectEnvDAL: Pick;
userDAL: Pick;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory;
licenseService: Pick;
secretApprovalRequestDAL: Pick;
+ secretApprovalPolicyEnvironmentDAL: TSecretApprovalPolicyEnvironmentDALFactory;
};
export type TSecretApprovalPolicyServiceFactory = ReturnType;
@@ -51,27 +53,30 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService,
secretApprovalPolicyApproverDAL,
secretApprovalPolicyBypasserDAL,
+ secretApprovalPolicyEnvironmentDAL,
projectEnvDAL,
userDAL,
licenseService,
secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const $policyExists = async ({
+ envIds,
envId,
secretPath,
policyId
}: {
- envId: string;
+ envIds?: string[];
+ envId?: string;
secretPath: string;
policyId?: string;
}) => {
- const policy = await secretApprovalPolicyDAL
- .findOne({
- envId,
- secretPath,
- deletedAt: null
- })
- .catch(() => null);
+ if (!envIds && !envId) {
+ throw new BadRequestError({ message: "At least one environment should be provided" });
+ }
+ const policy = await secretApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({
+ envIds: envId ? [envId] : envIds || [],
+ secretPath
+ });
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
@@ -88,6 +93,7 @@ export const secretApprovalPolicyServiceFactory = ({
projectId,
secretPath,
environment,
+ environments,
enforcementLevel,
allowedSelfApprovals
}: TCreateSapDTO) => {
@@ -127,17 +133,23 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
- const env = await projectEnvDAL.findOne({ slug: environment, projectId });
- if (!env) {
- throw new NotFoundError({
- message: `Environment with slug '${environment}' not found in project with ID ${projectId}`
- });
+ const mergedEnvs = (environment ? [environment] : environments) || [];
+ if (mergedEnvs.length === 0) {
+ throw new BadRequestError({ message: "Must provide either environment or environments" });
+ }
+ 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 })) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
- });
+ for (const env of envs) {
+ // eslint-disable-next-line no-await-in-loop
+ 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[] = [];
@@ -181,7 +193,7 @@ export const secretApprovalPolicyServiceFactory = ({
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.create(
{
- envId: env.id,
+ envId: envs[0].id,
approvals,
secretPath,
name,
@@ -190,6 +202,13 @@ export const secretApprovalPolicyServiceFactory = ({
},
tx
);
+ await secretApprovalPolicyEnvironmentDAL.insertMany(
+ envs.map((env) => ({
+ envId: env.id,
+ policyId: doc.id
+ })),
+ tx
+ );
let userApproverIds = userApprovers;
if (userApproverNames.length) {
@@ -253,12 +272,13 @@ export const secretApprovalPolicyServiceFactory = ({
return doc;
});
- return { ...secretApproval, environment: env, projectId };
+ return { ...secretApproval, environments: envs, projectId, environment: envs[0] };
};
const updateSecretApprovalPolicy = async ({
approvers,
bypassers,
+ environments,
secretPath,
name,
actorId,
@@ -288,17 +308,26 @@ export const secretApprovalPolicyServiceFactory = ({
message: `Secret approval policy with ID '${secretPolicyId}' not found`
});
}
-
+ let envs = secretApprovalPolicy.environments;
if (
- await $policyExists({
- envId: secretApprovalPolicy.envId,
- secretPath: secretPath || secretApprovalPolicy.secretPath,
- policyId: secretApprovalPolicy.id
- })
+ environments &&
+ (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'`
- });
+ envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: secretApprovalPolicy.projectId });
+ }
+ 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({
@@ -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);
if (bypasserUserIds.length) {
@@ -441,7 +481,8 @@ export const secretApprovalPolicyServiceFactory = ({
});
return {
...updatedSap,
- environment: secretApprovalPolicy.environment,
+ environments: secretApprovalPolicy.environments,
+ environment: secretApprovalPolicy.environments[0],
projectId: secretApprovalPolicy.projectId
};
};
@@ -487,7 +528,12 @@ export const secretApprovalPolicyServiceFactory = ({
const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx);
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 ({
@@ -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;
// this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter(
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
index ba5334e5c..80369e638 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
@@ -5,7 +5,8 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app
export type TCreateSapDTO = {
approvals: number;
secretPath: string;
- environment: string;
+ environment?: string;
+ environments?: string[];
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: (
| { type: BypasserType.Group; id: string }
@@ -29,6 +30,7 @@ export type TUpdateSapDTO = {
name?: string;
enforcementLevel?: EnforcementLevel;
allowedSelfApprovals?: boolean;
+ environments?: string[];
} & Omit;
export type TDeleteSapDTO = {
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
index c098d9b31..49f31bdf6 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
@@ -40,6 +40,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
+ .leftJoin(TableName.SecretApprovalPolicyEnvironment, (bd) => {
+ bd.on(
+ `${TableName.SecretApprovalPolicy}.id`,
+ "=",
+ `${TableName.SecretApprovalPolicyEnvironment}.policyId`
+ ).andOn(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", `${TableName.SecretFolder}.envId`);
+ })
.leftJoin(
db(TableName.Users).as("statusChangedByUser"),
`${TableName.SecretApprovalRequest}.statusChangedByUserId`,
@@ -146,7 +153,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
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("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index b5331e897..d1eefe7e3 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -538,6 +538,11 @@ export const secretApprovalRequestServiceFactory = ({
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({
actor: ActorType.USER,
diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts
index 3e6e5d265..1da1db376 100644
--- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts
+++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts
@@ -7,12 +7,13 @@ import {
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
executeWithPotentialGateway,
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
} from "@app/services/app-connection/shared/sql";
-import { generatePassword } from "../utils";
+import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../utils";
import {
TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection
@@ -32,6 +33,11 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
return redactedMessage;
};
+const ORACLE_PASSWORD_REQUIREMENTS = {
+ ...DEFAULT_PASSWORD_REQUIREMENTS,
+ length: 30
+};
+
export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials
@@ -43,6 +49,9 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
secretsMapping
} = secretRotation;
+ const passwordRequirement =
+ connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
+
const executeOperation = (
operation: (client: Knex) => Promise,
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
@@ -65,7 +74,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
try {
await executeOperation(async (client) => {
- await client.raw("SELECT 1");
+ await client.raw(connection.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
}, credentials);
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
@@ -75,11 +84,13 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
const issueCredentials: TRotationFactoryIssueCredentials = async (
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
// on issue to invalidate their existing passwords
const credentialsSet = [
- { username: username1, password: generatePassword() },
- { username: username2, password: generatePassword() }
+ { username: username1, password: generatePassword(passwordRequirement) },
+ { username: username2, password: generatePassword(passwordRequirement) }
];
try {
@@ -105,7 +116,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
credentialsToRevoke,
callback
) => {
- const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
+ const revokedCredentials = credentialsToRevoke.map(({ username }) => ({
+ username,
+ password: generatePassword(passwordRequirement)
+ }));
try {
await executeOperation(async (client) => {
@@ -128,7 +142,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
callback
) => {
// 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 {
await executeOperation(async (client) => {
diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts
index ef58687a1..4122abfda 100644
--- a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts
+++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts
@@ -11,7 +11,7 @@ type TPasswordRequirements = {
allowedSymbols?: string;
};
-const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
+export const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
length: 48,
required: {
lowercase: 1,
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index f3b4cbca0..71b6afb6b 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -2246,7 +2246,9 @@ export const AppConnections = {
},
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: {
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.",
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.`
+ },
+ RENDER: {
+ autoRedeployServices:
+ "Whether Infisical should automatically redeploy the configured Render service upon secret changes."
}
},
DESTINATION_CONFIG: {
diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts
index bc32d2b05..3fa2c0f82 100644
--- a/backend/src/lib/config/env.ts
+++ b/backend/src/lib/config/env.ts
@@ -272,10 +272,26 @@ const envSchema = z
// gcp app
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_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
SHOULD_USE_DATADOG_TRACER: 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,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
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>;
@@ -463,15 +495,54 @@ export const overwriteSchema: {
}
]
},
- azure: {
- name: "Azure",
+ azureAppConfiguration: {
+ name: "Azure App Configuration",
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."
},
{
- 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."
}
]
diff --git a/backend/src/lib/crypto/cryptography/crypto.ts b/backend/src/lib/crypto/cryptography/crypto.ts
index 967e7e007..b8fc45645 100644
--- a/backend/src/lib/crypto/cryptography/crypto.ts
+++ b/backend/src/lib/crypto/cryptography/crypto.ts
@@ -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 { isBase64 } from "../../base64";
-import { getConfig } from "../../config/env";
+import { getConfig, TEnvConfig } from "../../config/env";
import { CryptographyError } from "../../errors";
import { logger } from "../../logger";
import { asymmetricFipsValidated } from "./asymmetric-fips";
@@ -106,12 +106,12 @@ const cryptographyFactory = () => {
}
};
- const $setFipsModeEnabled = (enabled: boolean) => {
+ const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick) => {
// 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) {
crypto.setFips(true);
- const appCfg = getConfig();
+ const appCfg = envCfg || getConfig();
if (appCfg.ENCRYPTION_KEY) {
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
@@ -141,14 +141,14 @@ const cryptographyFactory = () => {
$isInitialized = true;
};
- const initialize = async (superAdminDAL: TSuperAdminDALFactory) => {
+ const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick) => {
if ($isInitialized) {
return isFipsModeEnabled();
}
if (process.env.FIPS_ENABLED !== "true") {
logger.info("Cryptography module initialized in normal operation mode.");
- $setFipsModeEnabled(false);
+ $setFipsModeEnabled(false, envCfg);
return false;
}
@@ -158,11 +158,11 @@ const cryptographyFactory = () => {
if (serverCfg) {
if (serverCfg.fipsEnabled) {
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
- $setFipsModeEnabled(true);
+ $setFipsModeEnabled(true, envCfg);
return true;
}
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
- $setFipsModeEnabled(false);
+ $setFipsModeEnabled(false, envCfg);
return false;
}
@@ -171,7 +171,7 @@ const cryptographyFactory = () => {
// 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.
- $setFipsModeEnabled(true);
+ $setFipsModeEnabled(true, envCfg);
return true;
};
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 01f58494b..0fb3191f8 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -11,6 +11,7 @@ import {
accessApprovalPolicyBypasserDALFactory
} 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 { 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 { 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";
@@ -76,6 +77,7 @@ import {
secretApprovalPolicyBypasserDALFactory
} 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 { 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 { 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";
@@ -425,9 +427,11 @@ export const registerRoutes = async (
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db);
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
+ const accessApprovalPolicyEnvironmentDAL = accessApprovalPolicyEnvironmentDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db);
+ const sapEnvironmentDAL = secretApprovalPolicyEnvironmentDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
@@ -561,6 +565,7 @@ export const registerRoutes = async (
projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
secretApprovalPolicyBypasserDAL: sapBypasserDAL,
+ secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL,
permissionService,
secretApprovalPolicyDAL,
licenseService,
@@ -1156,7 +1161,9 @@ export const registerRoutes = async (
keyStore,
licenseService,
projectDAL,
- folderDAL
+ folderDAL,
+ accessApprovalPolicyEnvironmentDAL,
+ secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL
});
const projectRoleService = projectRoleServiceFactory({
@@ -1318,6 +1325,7 @@ export const registerRoutes = async (
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL,
+ accessApprovalPolicyEnvironmentDAL,
groupDAL,
permissionService,
projectEnvDAL,
diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts
index 1a69f3845..8af4baa8b 100644
--- a/backend/src/server/routes/sanitizedSchemas.ts
+++ b/backend/src/server/routes/sanitizedSchemas.ts
@@ -93,6 +93,13 @@ export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
name: z.string(),
slug: z.string()
}),
+ environments: z.array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ slug: z.string()
+ })
+ ),
projectId: z.string()
})
);
diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts
index 937a8a84f..114794dc5 100644
--- a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts
+++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts
@@ -14,13 +14,13 @@ import {
} from "./azure-app-configuration-connection-types";
export const getAzureAppConfigurationConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID } = getConfig();
return {
name: "Azure App Configuration" as const,
app: AppConnection.AzureAppConfiguration as const,
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 { 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({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
@@ -47,8 +54,8 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
grant_type: "authorization_code",
code: inputCredentials.code,
scope: `openid offline_access https://azconfig.io/.default`,
- client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
+ client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts
index 338126c1e..eb0521c64 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts
@@ -1,3 +1,4 @@
export enum AzureClientSecretsConnectionMethod {
- OAuth = "oauth"
+ OAuth = "oauth",
+ ClientSecret = "client-secret"
}
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts
index 463e40e7c..41dbb4392 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-case-declarations */
import { AxiosError, AxiosResponse } from "axios";
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 {
ExchangeCodeAzureResponse,
+ TAzureClientSecretsConnectionClientSecretCredentials,
TAzureClientSecretsConnectionConfig,
TAzureClientSecretsConnectionCredentials
} from "./azure-client-secrets-connection-types";
export const getAzureClientSecretsConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID } = getConfig();
return {
name: "Azure Client Secrets" as const,
app: AppConnection.AzureClientSecrets as const,
- methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
- oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
+ methods: Object.values(AzureClientSecretsConnectionMethod) as [
+ AzureClientSecretsConnectionMethod.OAuth,
+ AzureClientSecretsConnectionMethod.ClientSecret
+ ],
+ oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID
};
};
@@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async (
kmsService: Pick
) => {
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);
if (!appConnection) {
@@ -63,104 +62,195 @@ export const getAzureConnectionAccessToken = async (
const { refreshToken } = credentials;
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(
+ 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(
- 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_ID,
- client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
- refresh_token: refreshToken
- })
- );
+ const updatedCredentials = {
+ ...credentials,
+ accessToken: data.access_token,
+ expiresAt: currentTime + data.expires_in * 1000,
+ refreshToken: data.refresh_token
+ };
- const updatedCredentials = {
- ...credentials,
- accessToken: data.access_token,
- expiresAt: currentTime + data.expires_in * 1000,
- refreshToken: data.refresh_token
- };
+ const encryptedCredentials = await encryptAppConnectionCredentials({
+ credentials: updatedCredentials,
+ orgId: appConnection.orgId,
+ kmsService
+ });
- const encryptedCredentials = await encryptAppConnectionCredentials({
- credentials: updatedCredentials,
- orgId: appConnection.orgId,
- kmsService
- });
+ await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
- 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(
+ 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) => {
const { credentials: inputCredentials, method } = config;
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
-
- if (!SITE_URL) {
- 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) {
- throw new InternalServerError({
- message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
- });
- }
-
- let tokenResp: AxiosResponse | null = null;
- let tokenError: AxiosError | null = null;
-
- try {
- tokenResp = await request.post(
- 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`
- });
- }
+ const {
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID,
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET,
+ SITE_URL
+ } = getConfig();
switch (method) {
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 | null = null;
+ let tokenError: AxiosError | null = null;
+
+ try {
+ tokenResp = await request.post(
+ 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 {
tenantId: inputCredentials.tenantId,
accessToken: tokenResp.data.access_token,
refreshToken: tokenResp.data.refresh_token,
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
};
+
+ case AzureClientSecretsConnectionMethod.ClientSecret:
+ const { tenantId, clientId, clientSecret } = inputCredentials;
+ try {
+ const { data: clientData } = await request.post(
+ 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:
throw new InternalServerError({
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts
index 2b4e65a13..d9f178a06 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts
@@ -26,6 +26,36 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
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", [
z.object({
method: z
@@ -34,6 +64,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
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
.object({
- credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
- AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
- )
+ credentials: z
+ .union([
+ AzureClientSecretsConnectionOAuthInputCredentialsSchema,
+ AzureClientSecretsConnectionClientSecretInputCredentialsSchema
+ ])
+ .optional()
+ .describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
@@ -59,6 +101,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
z.object({
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
+ }),
+ z.object({
+ method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
+ credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
})
])
);
@@ -69,6 +115,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true
})
+ }),
+ BaseAzureClientSecretsConnectionSchema.extend({
+ method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
+ credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
+ clientId: true,
+ tenantId: true
+ })
})
]);
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts
index fb20fbadd..1ad5a3411 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
+ AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
AzureClientSecretsConnectionSchema,
CreateAzureClientSecretsConnectionSchema,
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
>;
+export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
+ typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
+>;
+
export interface ExchangeCodeAzureResponse {
token_type: string;
scope: string;
diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts
index 644747353..e9bb1f6bd 100644
--- a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts
+++ b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts
@@ -23,7 +23,7 @@ import {
} from "./azure-devops-types";
export const getAzureDevopsConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID } = getConfig();
return {
name: "Azure DevOps" as const,
@@ -32,7 +32,7 @@ export const getAzureDevopsConnectionListItem = () => {
AzureDevOpsConnectionMethod.OAuth,
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) {
case AzureDevOpsConnectionMethod.OAuth:
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({
message: `Azure environment variables have not been configured`
});
@@ -81,8 +81,8 @@ export const getAzureDevopsConnection = async (
new URLSearchParams({
grant_type: "refresh_token",
scope: `https://app.vssps.visualstudio.com/.default`,
- client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID,
+ client_secret: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET,
refresh_token: refreshToken
})
);
@@ -119,7 +119,8 @@ export const getAzureDevopsConnection = async (
export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => {
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) {
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" });
}
- 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({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
@@ -144,8 +145,8 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
grant_type: "authorization_code",
code: oauthCredentials.code,
scope: `https://app.vssps.visualstudio.com/.default`,
- client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID,
+ client_secret: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts
index 116597ec4..95102c5d1 100644
--- a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts
+++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts
@@ -26,7 +26,10 @@ export const getAzureConnectionAccessToken = async (
kmsService: Pick
) => {
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({
message: `Azure environment variables have not been configured`
});
@@ -57,8 +60,8 @@ export const getAzureConnectionAccessToken = async (
new URLSearchParams({
grant_type: "refresh_token",
scope: `openid offline_access`,
- client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
+ client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
refresh_token: credentials.refreshToken
})
);
@@ -92,22 +95,23 @@ export const getAzureConnectionAccessToken = async (
};
export const getAzureKeyVaultConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID } = getConfig();
return {
name: "Azure Key Vault" as const,
app: AppConnection.AzureKeyVault as const,
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) => {
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({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
@@ -123,8 +127,8 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
grant_type: "authorization_code",
code: inputCredentials.code,
scope: `openid offline_access https://vault.azure.net/.default`,
- client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
+ client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts
index 9a82a6bbe..7fbe7343e 100644
--- a/backend/src/services/project-env/project-env-service.ts
+++ b/backend/src/services/project-env/project-env-service.ts
@@ -1,9 +1,11 @@
import { ForbiddenError } from "@casl/ability";
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
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 { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
@@ -20,6 +22,8 @@ type TProjectEnvServiceFactoryDep = {
permissionService: Pick;
licenseService: Pick;
keyStore: Pick;
+ accessApprovalPolicyEnvironmentDAL: Pick;
+ secretApprovalPolicyEnvironmentDAL: Pick;
};
export type TProjectEnvServiceFactory = ReturnType;
@@ -30,7 +34,9 @@ export const projectEnvServiceFactory = ({
licenseService,
keyStore,
projectDAL,
- folderDAL
+ folderDAL,
+ accessApprovalPolicyEnvironmentDAL,
+ secretApprovalPolicyEnvironmentDAL
}: TProjectEnvServiceFactoryDep) => {
const createEnvironment = async ({
projectId,
@@ -220,6 +226,20 @@ export const projectEnvServiceFactory = ({
}
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);
if (!doc)
throw new NotFoundError({
diff --git a/backend/src/services/reminder/reminder-queue.ts b/backend/src/services/reminder/reminder-queue.ts
index 1487a63f3..c038734bd 100644
--- a/backend/src/services/reminder/reminder-queue.ts
+++ b/backend/src/services/reminder/reminder-queue.ts
@@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({
{ pattern: "0 */1 * * *", utc: true },
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) => {
diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts
index 36e97e620..71347f998 100644
--- a/backend/src/services/secret-sync/render/render-sync-fns.ts
+++ b/backend/src/services/secret-sync/render/render-sync-fns.ts
@@ -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 = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
@@ -131,6 +153,10 @@ export const RenderSyncFns = {
}
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
+
+ if (secretSync.syncOptions.autoRedeployServices) {
+ await redeployService(secretSync);
+ }
},
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise => {
@@ -151,5 +177,9 @@ export const RenderSyncFns = {
}
}
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
+
+ if (secretSync.syncOptions.autoRedeployServices) {
+ await redeployService(secretSync);
+ }
}
};
diff --git a/backend/src/services/secret-sync/render/render-sync-schemas.ts b/backend/src/services/secret-sync/render/render-sync-schemas.ts
index 77414c17c..0d6e93987 100644
--- a/backend/src/services/secret-sync/render/render-sync-schemas.ts
+++ b/backend/src/services/secret-sync/render/render-sync-schemas.ts
@@ -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 };
-export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({
+export const RenderSyncSchema = BaseSecretSyncSchema(
+ SecretSync.Render,
+ RenderSyncOptionsConfig,
+ RenderSyncOptionsSchema
+).extend({
destination: z.literal(SecretSync.Render),
destinationConfig: RenderSyncDestinationConfigSchema
});
export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Render,
- RenderSyncOptionsConfig
+ RenderSyncOptionsConfig,
+ RenderSyncOptionsSchema
).extend({
destinationConfig: RenderSyncDestinationConfigSchema
});
export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Render,
- RenderSyncOptionsConfig
+ RenderSyncOptionsConfig,
+ RenderSyncOptionsSchema
).extend({
destinationConfig: RenderSyncDestinationConfigSchema.optional()
});
diff --git a/docs/images/app-connections/azure/client-secrets/create-client-secrets-method.png b/docs/images/app-connections/azure/client-secrets/create-client-secrets-method.png
new file mode 100644
index 000000000..63717c547
Binary files /dev/null and b/docs/images/app-connections/azure/client-secrets/create-client-secrets-method.png differ
diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx
index 959a1812a..679839878 100644
--- a/docs/integrations/app-connections/azure-app-configuration.mdx
+++ b/docs/integrations/app-connections/azure-app-configuration.mdx
@@ -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.
- - `INF_APP_CONNECTION_AZURE_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_ID`: The **Application (Client) ID** 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.
diff --git a/docs/integrations/app-connections/azure-client-secrets.mdx b/docs/integrations/app-connections/azure-client-secrets.mdx
index 55416303a..1fb1c753f 100644
--- a/docs/integrations/app-connections/azure-client-secrets.mdx
+++ b/docs/integrations/app-connections/azure-client-secrets.mdx
@@ -43,12 +43,6 @@ Infisical currently only supports one method for connecting to Azure, which is O
- `Application.ReadWrite.All` (Delegated)
- `Directory.ReadWrite.All` (Delegated)
- `User.Read` (Delegated)
- - Azure App Configuration
- - `KeyValue.Delete` (Delegated)
- - `KeyValue.Read` (Delegated)
- - `KeyValue.Write` (Delegated)
- - Access Key Vault
- - `user_impersonation` (Delegated)

@@ -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.
- - `INF_APP_CONNECTION_AZURE_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_ID`: The **Application (Client) ID** 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.
@@ -72,6 +66,30 @@ Infisical currently only supports one method for connecting to Azure, which is O
+
+ Ensure your Azure application has the required permissions that Infisical needs for the Azure Client Secrets connection to work.
+
+ **Prerequisites:**
+ - An active Azure setup.
+
+
+
+ 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)
+
+ 
+
+
+
+
## Setup Azure Connection in Infisical
@@ -82,21 +100,31 @@ Infisical currently only supports one method for connecting to Azure, which is O
Select the **Azure Connection** option from the connection options modal. 
-
- Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
+
+
+
+
+ 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**.
- 
+ 
+
+
+ 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. 
+
+
+
+
+ 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.
-
-
-
-
- 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. 
-
+ 
+
+
+
+
Your **Azure Client Secrets Connection** is now available for use. 
diff --git a/docs/integrations/app-connections/azure-devops.mdx b/docs/integrations/app-connections/azure-devops.mdx
index 8fcc25427..6a9e71430 100644
--- a/docs/integrations/app-connections/azure-devops.mdx
+++ b/docs/integrations/app-connections/azure-devops.mdx
@@ -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.
- - `INF_APP_CONNECTION_AZURE_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_ID`: The **Application (Client) ID** 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.
diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx
index f73dab834..22cdcf637 100644
--- a/docs/integrations/app-connections/azure-key-vault.mdx
+++ b/docs/integrations/app-connections/azure-key-vault.mdx
@@ -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.
- - `INF_APP_CONNECTION_AZURE_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_ID`: The **Application (Client) ID** 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.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index edbf5673f..3c73d9fe3 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -55,7 +55,7 @@
"@ucast/mongo2js": "^1.3.4",
"@xyflow/react": "^12.4.4",
"argon2-browser": "^1.18.0",
- "axios": "^1.7.9",
+ "axios": "^1.11.0",
"classnames": "^2.5.1",
"cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0",
@@ -5282,13 +5282,13 @@
}
},
"node_modules/axios": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
- "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -5700,7 +5700,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -6665,7 +6664,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
"integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
@@ -6819,7 +6817,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6829,7 +6826,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6867,7 +6863,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -6877,15 +6872,15 @@
}
},
"node_modules/es-set-tostringtag": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
- "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
- "dev": true,
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
- "get-intrinsic": "^1.2.4",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
- "hasown": "^2.0.1"
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -7855,13 +7850,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
- "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -7992,7 +7989,6 @@
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -8139,7 +8135,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8215,7 +8210,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8228,7 +8222,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -9545,7 +9538,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
"integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
diff --git a/frontend/package.json b/frontend/package.json
index 9a2cc2ba4..1bc55c1c7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -59,7 +59,7 @@
"@ucast/mongo2js": "^1.3.4",
"@xyflow/react": "^12.4.4",
"argon2-browser": "^1.18.0",
- "axios": "^1.7.9",
+ "axios": "^1.11.0",
"classnames": "^2.5.1",
"cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0",
diff --git a/frontend/src/components/permissions/OrgPermissionCan.tsx b/frontend/src/components/permissions/OrgPermissionCan.tsx
index bbe5bf986..d2e698e88 100644
--- a/frontend/src/components/permissions/OrgPermissionCan.tsx
+++ b/frontend/src/components/permissions/OrgPermissionCan.tsx
@@ -1,26 +1,14 @@
import { FunctionComponent, ReactNode } from "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 { Tooltip } from "../v2";
+import { AccessRestrictedBanner, Tooltip } from "../v2";
export const OrgPermissionGuardBanner = () => {
return (
-
-
-
-
-
-
Access Restricted
-
- Your role has limited permissions, please
contact your admin to gain access
-
-
-
+
);
};
diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx
index b3c7a4f53..3f86a7257 100644
--- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx
+++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx
@@ -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 { AccessRestrictedBanner } from "@app/components/v2";
+
type Props = {
containerClassName?: string;
- className?: string;
- children?: ReactNode;
};
-export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => {
+export const PermissionDeniedBanner = ({ containerClassName }: Props) => {
return (
-
-
-
-
-
-
-
Access Restricted
- {children || (
-
- Your role has limited permissions, please
contact your administrator to gain
- access
-
- )}
-
-
-
+
);
};
diff --git a/frontend/src/components/permissions/ProjectPermissionCan.tsx b/frontend/src/components/permissions/ProjectPermissionCan.tsx
index f49709ff3..88a8c6ad9 100644
--- a/frontend/src/components/permissions/ProjectPermissionCan.tsx
+++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx
@@ -1,9 +1,8 @@
import { FunctionComponent, ReactNode } from "react";
import { AbilityTuple, MongoAbility } from "@casl/ability";
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 { Tooltip } from "../v2/Tooltip";
@@ -11,17 +10,7 @@ import { Tooltip } from "../v2/Tooltip";
export const ProjectPermissionGuardBanner = () => {
return (
-
-
-
-
-
-
Access Restricted
-
- Your role has limited permissions, please
contact your admin to gain access
-
-
-
+
);
};
diff --git a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx
index 210257d7d..f5b9a39b3 100644
--- a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx
+++ b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx
@@ -76,7 +76,6 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
)
}
- onPointerDownOutside={(e) => e.preventDefault()}
className={selectedRotation ? "max-w-2xl" : "max-w-3xl"}
subTitle={
selectedRotation ? undefined : "Select a provider to create a secret rotation for."
diff --git a/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx b/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx
index a3943cf35..c95baaae5 100644
--- a/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx
+++ b/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx
@@ -75,7 +75,6 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
)
}
- onPointerDownOutside={(e) => e.preventDefault()}
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
subTitle={
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
diff --git a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx
index 7bae0479f..5ff72e810 100644
--- a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx
+++ b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx
@@ -56,7 +56,6 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
"Add Sync"
)
}
- onPointerDownOutside={(e) => e.preventDefault()}
className="max-w-2xl"
bodyClassName="overflow-visible"
subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."}
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx
index b5cb407cb..3d3f8df93 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx
@@ -9,7 +9,7 @@ import {
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
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";
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx
new file mode 100644
index 000000000..6d4173bd0
--- /dev/null
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx
@@ -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();
+
+ return (
+ (
+
+
+ Auto Redeploy Services On Sync
+ If enabled, services will be automatically redeployed upon secret changes.
+ }
+ >
+
+
+
+
+ )}
+ />
+ );
+};
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
index 50ea46ac0..cb7a40559 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
@@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
+import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields";
type Props = {
hideInitialSync?: boolean;
@@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsFieldsComponent = ;
break;
+ case SecretSync.Render:
+ AdditionalSyncOptionsFieldsComponent = ;
+ break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault:
@@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.OnePass:
case SecretSync.OCIVault:
case SecretSync.Heroku:
- case SecretSync.Render:
case SecretSync.Flyio:
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx
index becc46c1d..15f5b6625 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx
@@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
+import { Badge } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
+export const RenderSyncOptionsReviewFields = () => {
+ const { watch } = useFormContext();
+
+ const [{ autoRedeployServices }] = watch(["syncOptions"]);
+
+ return (
+
+ {autoRedeployServices ? (
+
+ Enabled
+
+ ) : (
+
+ Disabled
+
+ )}
+
+ );
+};
+
export const RenderSyncReviewFields = () => {
const { watch } = useFormContext();
const serviceName = watch("destinationConfig.serviceName");
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
index c1194a4c1..5ee53f2e3 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
@@ -35,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
-import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
+import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields";
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
@@ -121,6 +121,7 @@ export const SecretSyncReviewFields = () => {
break;
case SecretSync.Render:
DestinationFieldsComponent = ;
+ AdditionalSyncOptionsFieldsComponent = ;
break;
case SecretSync.Flyio:
DestinationFieldsComponent = ;
diff --git a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts
index 16b213421..83e5347eb 100644
--- a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts
+++ b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts
@@ -2,9 +2,13 @@ import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
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({
destination: z.literal(SecretSync.Render),
destinationConfig: z.discriminatedUnion("scope", [
diff --git a/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx b/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx
new file mode 100644
index 000000000..1979039dd
--- /dev/null
+++ b/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx
@@ -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't provide access to this feature.
+
Contact your administrator to request access.
+ >
+ )
+}: Props) => {
+ return (
+
+ );
+};
diff --git a/frontend/src/components/v2/AccessRestrictedBanner/index.ts b/frontend/src/components/v2/AccessRestrictedBanner/index.ts
new file mode 100644
index 000000000..d60468572
--- /dev/null
+++ b/frontend/src/components/v2/AccessRestrictedBanner/index.ts
@@ -0,0 +1 @@
+export * from "./AccessRestrictedBanner";
diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx
index 1256cf005..8019c3589 100644
--- a/frontend/src/components/v2/index.tsx
+++ b/frontend/src/components/v2/index.tsx
@@ -1,4 +1,5 @@
/* eslint-disable react-refresh/only-export-components */
+export * from "./AccessRestrictedBanner";
export * from "./Accordion";
export * from "./Alert";
export * from "./Badge";
diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts
index 1345cb493..8c8475767 100644
--- a/frontend/src/helpers/appConnections.ts
+++ b/frontend/src/helpers/appConnections.ts
@@ -171,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case RenderConnectionMethod.ApiKey:
case ChecklyConnectionMethod.ApiKey:
return { name: "API Key", icon: faKey };
-
+ case AzureClientSecretsConnectionMethod.ClientSecret:
+ return { name: "Client Secret", icon: faKey };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}
diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts
index 0eb41e119..b4c0df352 100644
--- a/frontend/src/helpers/secretSyncs.ts
+++ b/frontend/src/helpers/secretSyncs.ts
@@ -4,9 +4,9 @@ import {
SecretSyncImportBehavior,
SecretSyncInitialSyncBehavior
} 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 { 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.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx
index ef814d958..529a74930 100644
--- a/frontend/src/hoc/withPermission/withPermission.tsx
+++ b/frontend/src/hoc/withPermission/withPermission.tsx
@@ -1,9 +1,8 @@
import { ComponentType } from "react";
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 { AccessRestrictedBanner } from "@app/components/v2";
import { TOrgPermission, useOrgPermission } from "@app/context";
type Props = (T extends AbilityTuple
@@ -14,11 +13,11 @@ type Props = (T extends AbilityTuple
: {
action: string;
subject: string;
- }) & { className?: string; containerClassName?: string };
+ }) & { containerClassName?: string };
export const withPermission = (
Component: ComponentType,
- { action, subject, className, containerClassName }: Props["abilities"]>
+ { action, subject, containerClassName }: Props["abilities"]>
) => {
const HOC = (hocProps: T) => {
const { permission } = useOrgPermission();
@@ -33,22 +32,7 @@ export const withPermission = (
containerClassName
)}
>
-
-
-
-
-
-
Access Restricted
-
- Your role has limited permissions, please
contact your admin to gain access
-
-
-
+
);
}
diff --git a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx
index 564d44adc..7ba6efc57 100644
--- a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx
+++ b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx
@@ -1,14 +1,12 @@
import { ComponentType } from "react";
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 { AccessRestrictedBanner } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
type Props = {
- className?: string;
containerClassName?: string;
action: T[0];
subject: T[1];
@@ -16,7 +14,7 @@ type Props = {
export const withProjectPermission = (
Component: ComponentType, "action" | "subject"> & T>,
- { action, subject, className, containerClassName }: Props
+ { action, subject, containerClassName }: Props
) => {
const HOC = (hocProps: Omit, "action" | "subject"> & T) => {
const { permission } = useProjectPermission();
@@ -31,23 +29,7 @@ export const withProjectPermission = (
containerClassName
)}
>
-
-
-
-
-
-
Permission Denied
-
- You do not have permission to this page.
Kindly contact your organization
- administrator
-
-
-
+
);
}
diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx
index 3b05ff61b..ad7917a8f 100644
--- a/frontend/src/hooks/api/accessApproval/mutation.tsx
+++ b/frontend/src/hooks/api/accessApproval/mutation.tsx
@@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => {
return useMutation