mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-03 20:23:35 +00:00
Compare commits
33 Commits
fix/oracle
...
docs/terra
Author | SHA1 | Date | |
---|---|---|---|
|
0ccc279805 | ||
|
7b160e0014 | ||
|
c31ede99b1 | ||
|
71f609fa4f | ||
|
8b963479a9 | ||
|
932e87f3e4 | ||
|
4f51ade2cd | ||
|
f3216800eb | ||
|
7d1bc86702 | ||
|
975b621bc8 | ||
|
ba9da3e6ec | ||
|
d2274a622a | ||
|
41ba7edba2 | ||
|
7acefbca29 | ||
|
e246f6bbfe | ||
|
f265fa6d37 | ||
|
8eebd7228f | ||
|
2a5593ea30 | ||
|
17af33372c | ||
|
27da14df9d | ||
|
cd4b9cd03a | ||
|
0779091d1f | ||
|
c421057cf1 | ||
|
3400a8f911 | ||
|
e6588b5d0e | ||
|
608979efa7 | ||
|
99e8bdef58 | ||
|
4e960445a4 | ||
|
7af5a4ad8d | ||
|
b05ea8a69a | ||
|
0d97bb4c8c | ||
|
60657f0bc6 | ||
|
05408bc151 |
13
.env.example
13
.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=
|
||||
|
31
backend/package-lock.json
generated
31
backend/package-lock.json
generated
@@ -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": {
|
||||
|
@@ -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",
|
||||
|
21
backend/src/@types/knex.d.ts
vendored
21
backend/src/@types/knex.d.ts
vendored
@@ -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,
|
||||
|
@@ -0,0 +1,96 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicyEnvironment, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
t.uuid("envId").notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment);
|
||||
t.timestamps(true, true, true);
|
||||
t.unique(["policyId", "envId"]);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment);
|
||||
|
||||
const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalPolicy))
|
||||
.whereNotNull(`${TableName.AccessApprovalPolicy}.envId`);
|
||||
|
||||
const accessApprovalPolicies = existingAccessApprovalPolicies.map(async (policy) => {
|
||||
await knex(TableName.AccessApprovalPolicyEnvironment).insert({
|
||||
policyId: policy.id,
|
||||
envId: policy.envId
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(accessApprovalPolicies);
|
||||
}
|
||||
if (!(await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment))) {
|
||||
await knex.schema.createTable(TableName.SecretApprovalPolicyEnvironment, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.SecretApprovalPolicy).onDelete("CASCADE");
|
||||
t.uuid("envId").notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment);
|
||||
t.timestamps(true, true, true);
|
||||
t.unique(["policyId", "envId"]);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment);
|
||||
|
||||
const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
|
||||
.whereNotNull(`${TableName.SecretApprovalPolicy}.envId`);
|
||||
|
||||
const secretApprovalPolicies = existingSecretApprovalPolicies.map(async (policy) => {
|
||||
await knex(TableName.SecretApprovalPolicyEnvironment).insert({
|
||||
policyId: policy.id,
|
||||
envId: policy.envId
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(secretApprovalPolicies);
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
|
||||
// Add the new foreign key constraint with ON DELETE SET NULL
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
|
||||
// Add the new foreign key constraint with ON DELETE SET NULL
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment)) {
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyEnvironment);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment);
|
||||
}
|
||||
if (await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment)) {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretApprovalPolicyEnvironment);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment);
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
});
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalPoliciesEnvironmentsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPoliciesEnvironments = z.infer<typeof AccessApprovalPoliciesEnvironmentsSchema>;
|
||||
export type TAccessApprovalPoliciesEnvironmentsInsert = Omit<
|
||||
z.input<typeof AccessApprovalPoliciesEnvironmentsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalPoliciesEnvironmentsUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesEnvironmentsSchema>, TImmutableDBKeys>
|
||||
>;
|
@@ -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",
|
||||
|
@@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretApprovalPoliciesEnvironmentsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPoliciesEnvironments = z.infer<typeof SecretApprovalPoliciesEnvironmentsSchema>;
|
||||
export type TSecretApprovalPoliciesEnvironmentsInsert = Omit<
|
||||
z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TSecretApprovalPoliciesEnvironmentsUpdate = Partial<
|
||||
Omit<z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>, TImmutableDBKeys>
|
||||
>;
|
@@ -17,52 +17,66 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
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(),
|
||||
|
@@ -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({
|
||||
|
@@ -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<TAccessApprovalPolicies & { projectId: string }>,
|
||||
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
|
||||
};
|
||||
};
|
||||
|
@@ -0,0 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalPolicyEnvironmentDALFactory = ReturnType<typeof accessApprovalPolicyEnvironmentDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyEnvironmentOrm = ormify(db, TableName.AccessApprovalPolicyEnvironment);
|
||||
|
||||
const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicyEnvironment)
|
||||
.join(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalPolicyEnvironment}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ envId }, TableName.AccessApprovalPolicyEnvironment))
|
||||
.whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalPolicyEnvironment));
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId };
|
||||
};
|
@@ -21,6 +21,7 @@ import {
|
||||
TAccessApprovalPolicyBypasserDALFactory
|
||||
} 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<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
|
||||
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 {
|
||||
|
@@ -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<TProjectPermission, "projectId">;
|
||||
|
||||
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: (
|
||||
| {
|
||||
|
@@ -65,7 +65,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
deletedAt: Date | null | undefined;
|
||||
};
|
||||
projectId: string;
|
||||
environment: string;
|
||||
environments: string[];
|
||||
requestedByUser: {
|
||||
userId: string;
|
||||
email: string | null | undefined;
|
||||
@@ -515,7 +515,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
`accessApprovalReviewerUser.id`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyEnvironment,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyEnvironment}.policyId`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.Environment,
|
||||
`${TableName.AccessApprovalPolicyEnvironment}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
||||
@@ -683,6 +693,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "environment",
|
||||
label: "environments" as const,
|
||||
mapper: ({ environment }) => environment
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@@ -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<string, unknown>;
|
||||
if (typeof metadata === "object" && metadata !== null && "environment" in metadata) {
|
||||
const env = metadata.environment;
|
||||
return typeof env === "string" ? env : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({
|
||||
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"
|
||||
},
|
||||
|
@@ -23,6 +23,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
|
||||
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<TSecretApprovalPolicies & { projectId: string }>,
|
||||
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 };
|
||||
};
|
||||
|
@@ -0,0 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TSecretApprovalPolicyEnvironmentDALFactory = ReturnType<typeof secretApprovalPolicyEnvironmentDALFactory>;
|
||||
|
||||
export const secretApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalPolicyEnvironmentOrm = ormify(db, TableName.SecretApprovalPolicyEnvironment);
|
||||
|
||||
const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicyEnvironment)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalPolicyEnvironment}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ envId }, TableName.SecretApprovalPolicyEnvironment))
|
||||
.whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalPolicyEnvironment));
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId };
|
||||
};
|
@@ -19,6 +19,7 @@ import {
|
||||
TSecretApprovalPolicyBypasserDALFactory
|
||||
} 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<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||
secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">;
|
||||
secretApprovalPolicyEnvironmentDAL: TSecretApprovalPolicyEnvironmentDALFactory;
|
||||
};
|
||||
|
||||
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
||||
@@ -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(
|
||||
|
@@ -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<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteSapDTO = {
|
||||
|
@@ -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<TUsers>(
|
||||
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"),
|
||||
|
@@ -537,6 +537,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,
|
||||
|
@@ -261,10 +261,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"),
|
||||
@@ -341,7 +357,23 @@ const envSchema = z
|
||||
isHsmConfigured:
|
||||
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(",")
|
||||
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","),
|
||||
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID:
|
||||
data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET:
|
||||
data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID:
|
||||
data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET:
|
||||
data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID:
|
||||
data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET:
|
||||
data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID:
|
||||
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET:
|
||||
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET
|
||||
}));
|
||||
|
||||
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
|
||||
@@ -451,15 +483,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."
|
||||
}
|
||||
]
|
||||
|
@@ -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({
|
||||
@@ -1317,6 +1324,7 @@ export const registerRoutes = async (
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
accessApprovalPolicyBypasserDAL,
|
||||
accessApprovalPolicyEnvironmentDAL,
|
||||
groupDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
|
@@ -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()
|
||||
})
|
||||
);
|
||||
|
@@ -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`
|
||||
})
|
||||
);
|
||||
|
@@ -23,7 +23,7 @@ import {
|
||||
} 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,
|
||||
@@ -32,7 +32,7 @@ export const getAzureClientSecretsConnectionListItem = () => {
|
||||
AzureClientSecretsConnectionMethod.OAuth,
|
||||
AzureClientSecretsConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID
|
||||
};
|
||||
};
|
||||
|
||||
@@ -64,7 +64,10 @@ export const getAzureConnectionAccessToken = async (
|
||||
const currentTime = Date.now();
|
||||
switch (appConnection.method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
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`
|
||||
});
|
||||
@@ -74,8 +77,8 @@ export const getAzureConnectionAccessToken = async (
|
||||
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,
|
||||
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
|
||||
})
|
||||
);
|
||||
@@ -142,7 +145,11 @@ export const getAzureConnectionAccessToken = async (
|
||||
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();
|
||||
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:
|
||||
@@ -150,7 +157,10 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
||||
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_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`
|
||||
});
|
||||
@@ -166,8 +176,8 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
||||
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,
|
||||
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`
|
||||
})
|
||||
);
|
||||
|
@@ -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`
|
||||
})
|
||||
);
|
||||
|
@@ -26,7 +26,10 @@ export const getAzureConnectionAccessToken = async (
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
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`
|
||||
})
|
||||
);
|
||||
|
@@ -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<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
|
||||
accessApprovalPolicyEnvironmentDAL: Pick<TAccessApprovalPolicyEnvironmentDALFactory, "findAvailablePoliciesByEnvId">;
|
||||
secretApprovalPolicyEnvironmentDAL: Pick<TSecretApprovalPolicyEnvironmentDALFactory, "findAvailablePoliciesByEnvId">;
|
||||
};
|
||||
|
||||
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
|
||||
@@ -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({
|
||||
|
@@ -32,6 +32,7 @@
|
||||
"documentation/guides/python",
|
||||
"documentation/guides/nextjs-vercel",
|
||||
"documentation/guides/microsoft-power-apps",
|
||||
"documentation/guides/terraform",
|
||||
"documentation/guides/organization-structure"
|
||||
]
|
||||
},
|
||||
|
2453
docs/documentation/guides/terraform.mdx
Normal file
2453
docs/documentation/guides/terraform.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
</Step>
|
||||
|
@@ -57,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.
|
||||
</Step>
|
||||
|
@@ -56,8 +56,8 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a
|
||||
|
||||
Back in your Infisical instance, add two new environment variables for the credentials of your Azure application.
|
||||
|
||||
- `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.
|
||||
</Step>
|
||||
|
@@ -49,8 +49,8 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
|
||||
Back in your Infisical instance, add two new environment variables for the credentials of your Azure application.
|
||||
|
||||
- `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.
|
||||
</Step>
|
||||
|
40
frontend/package-lock.json
generated
40
frontend/package-lock.json
generated
@@ -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"
|
||||
|
@@ -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",
|
||||
|
@@ -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 (
|
||||
<div className="container mx-auto flex h-full items-center justify-center">
|
||||
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your admin to gain access
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessRestrictedBanner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,15 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { 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 (
|
||||
<div
|
||||
className={twMerge(
|
||||
@@ -17,22 +14,7 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div className={twMerge("rounded-md bg-mineshaft-800 p-16 text-bunker-300", className)}>
|
||||
<div className="flex items-end space-x-12">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
{children || (
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your administrator to gain
|
||||
access
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessRestrictedBanner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -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 (
|
||||
<div className="container mx-auto flex h-full items-center justify-center">
|
||||
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your admin to gain access
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessRestrictedBanner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -76,7 +76,6 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className={selectedRotation ? "max-w-2xl" : "max-w-3xl"}
|
||||
subTitle={
|
||||
selectedRotation ? undefined : "Select a provider to create a secret rotation for."
|
||||
|
@@ -75,7 +75,6 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
|
||||
subTitle={
|
||||
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
|
||||
|
@@ -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."}
|
||||
|
@@ -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.
|
||||
<br /> Contact your administrator to request access.
|
||||
</>
|
||||
)
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex items-center rounded-md border border-mineshaft-500 bg-gradient-to-br from-mineshaft-900 to-mineshaft-600 px-16 py-12 text-center text-bunker-300">
|
||||
<div>
|
||||
<div className="text-4xl font-medium text-bunker-100">{title}</div>
|
||||
<div className="-mt-1 text-sm">{body}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from "./AccessRestrictedBanner";
|
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
export * from "./AccessRestrictedBanner";
|
||||
export * from "./Accordion";
|
||||
export * from "./Alert";
|
||||
export * from "./Badge";
|
||||
|
@@ -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 Abilities> = (T extends AbilityTuple
|
||||
@@ -14,11 +13,11 @@ type Props<T extends Abilities> = (T extends AbilityTuple
|
||||
: {
|
||||
action: string;
|
||||
subject: string;
|
||||
}) & { className?: string; containerClassName?: string };
|
||||
}) & { containerClassName?: string };
|
||||
|
||||
export const withPermission = <T extends object, J extends TOrgPermission>(
|
||||
Component: ComponentType<T>,
|
||||
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
|
||||
{ action, subject, containerClassName }: Props<Generics<J>["abilities"]>
|
||||
) => {
|
||||
const HOC = (hocProps: T) => {
|
||||
const { permission } = useOrgPermission();
|
||||
@@ -33,22 +32,7 @@ export const withPermission = <T extends object, J extends TOrgPermission>(
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your admin to gain access
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessRestrictedBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -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<T extends AbilityTuple> = {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
action: T[0];
|
||||
subject: T[1];
|
||||
@@ -16,7 +14,7 @@ type Props<T extends AbilityTuple> = {
|
||||
|
||||
export const withProjectPermission = <T extends object>(
|
||||
Component: ComponentType<Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T>,
|
||||
{ action, subject, className, containerClassName }: Props<ProjectPermissionSet>
|
||||
{ action, subject, containerClassName }: Props<ProjectPermissionSet>
|
||||
) => {
|
||||
const HOC = (hocProps: Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T) => {
|
||||
const { permission } = useProjectPermission();
|
||||
@@ -31,23 +29,7 @@ export const withProjectPermission = <T extends object>(
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Permission Denied</div>
|
||||
<div className="text-sm">
|
||||
You do not have permission to this page. <br /> Kindly contact your organization
|
||||
administrator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessRestrictedBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => {
|
||||
|
||||
return useMutation<object, object, TCreateAccessPolicyDTO>({
|
||||
mutationFn: async ({
|
||||
environment,
|
||||
environments,
|
||||
projectSlug,
|
||||
approvals,
|
||||
approvers,
|
||||
@@ -29,7 +29,7 @@ export const useCreateAccessApprovalPolicy = () => {
|
||||
approvalsRequired
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
||||
environment,
|
||||
environments,
|
||||
projectSlug,
|
||||
approvals,
|
||||
bypassers,
|
||||
@@ -63,7 +63,8 @@ export const useUpdateAccessApprovalPolicy = () => {
|
||||
secretPath,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals,
|
||||
approvalsRequired
|
||||
approvalsRequired,
|
||||
environments
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
||||
approvals,
|
||||
@@ -73,7 +74,8 @@ export const useUpdateAccessApprovalPolicy = () => {
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals,
|
||||
approvalsRequired
|
||||
approvalsRequired,
|
||||
environments
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
@@ -8,9 +8,8 @@ export type TAccessApprovalPolicy = {
|
||||
name: string;
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
envId: string;
|
||||
workspace: string;
|
||||
environment: WorkspaceEnv;
|
||||
environments: WorkspaceEnv[];
|
||||
projectId: string;
|
||||
policyType: PolicyType;
|
||||
approversRequired: boolean;
|
||||
@@ -166,7 +165,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
|
||||
export type TCreateAccessPolicyDTO = {
|
||||
projectSlug: string;
|
||||
name?: string;
|
||||
environment: string;
|
||||
environments: string[];
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
approvals?: number;
|
||||
@@ -182,7 +181,7 @@ export type TUpdateAccessPolicyDTO = {
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
secretPath?: string;
|
||||
environment?: string;
|
||||
environments?: string[];
|
||||
approvals?: number;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
|
@@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => {
|
||||
|
||||
return useMutation<object, object, TCreateSecretPolicyDTO>({
|
||||
mutationFn: async ({
|
||||
environment,
|
||||
environments,
|
||||
workspaceId,
|
||||
approvals,
|
||||
approvers,
|
||||
@@ -21,7 +21,7 @@ export const useCreateSecretApprovalPolicy = () => {
|
||||
allowedSelfApprovals
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
||||
environment,
|
||||
environments,
|
||||
workspaceId,
|
||||
approvals,
|
||||
approvers,
|
||||
@@ -53,7 +53,8 @@ export const useUpdateSecretApprovalPolicy = () => {
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
allowedSelfApprovals,
|
||||
environments
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||
approvals,
|
||||
@@ -62,7 +63,8 @@ export const useUpdateSecretApprovalPolicy = () => {
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
allowedSelfApprovals,
|
||||
environments
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
@@ -5,8 +5,7 @@ export type TSecretApprovalPolicy = {
|
||||
id: string;
|
||||
workspace: string;
|
||||
name: string;
|
||||
envId: string;
|
||||
environment: WorkspaceEnv;
|
||||
environments: WorkspaceEnv[];
|
||||
secretPath?: string;
|
||||
approvals: number;
|
||||
approvers: Approver[];
|
||||
@@ -48,7 +47,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
|
||||
export type TCreateSecretPolicyDTO = {
|
||||
workspaceId: string;
|
||||
name?: string;
|
||||
environment: string;
|
||||
environments: string[];
|
||||
secretPath: string;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
@@ -68,6 +67,7 @@ export type TUpdateSecretPolicyDTO = {
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
// for invalidating list
|
||||
workspaceId: string;
|
||||
environments?: string[];
|
||||
};
|
||||
|
||||
export type TDeleteSecretPolicyDTO = {
|
||||
|
@@ -49,7 +49,8 @@ export const usePathAccessPolicies = ({ secretPath, environment }: Params) => {
|
||||
return useMemo(() => {
|
||||
const pathPolicies = policies?.filter(
|
||||
(policy) =>
|
||||
policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath)
|
||||
policy.environments?.some((env) => env.slug === environment) &&
|
||||
matchesPath(secretPath, policy.secretPath)
|
||||
);
|
||||
|
||||
return {
|
||||
|
@@ -22,7 +22,6 @@ export const KmipPage = () => {
|
||||
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
passThrough={false}
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionKmipActions.ReadClients}
|
||||
a={ProjectPermissionSub.Kmip}
|
||||
|
@@ -22,7 +22,6 @@ export const OverviewPage = () => {
|
||||
description="Manage keys and perform cryptographic operations."
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
passThrough={false}
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Cmek}
|
||||
|
@@ -155,8 +155,8 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
|
||||
const selectablePaths = useMemo(() => {
|
||||
if (!policies) return [];
|
||||
const environmentPolicies = policies.filter(
|
||||
(policy) => policy.environment.slug === selectedEnvironment
|
||||
const environmentPolicies = policies.filter((policy) =>
|
||||
policy.environments.find((env) => env.slug === selectedEnvironment)
|
||||
);
|
||||
|
||||
privilegeForm.setValue("secretPath", "", {
|
||||
|
@@ -166,17 +166,20 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
const filteredPolicies = useMemo(
|
||||
() =>
|
||||
policies
|
||||
.filter(({ policyType, environment, name, secretPath }) => {
|
||||
.filter(({ policyType, environments, name, secretPath }) => {
|
||||
if (filters.type && policyType !== filters.type) return false;
|
||||
|
||||
if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id))
|
||||
if (
|
||||
filters.environmentIds.length &&
|
||||
!environments.some((env) => filters.environmentIds.includes(env.id))
|
||||
)
|
||||
return false;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) ||
|
||||
environment.name.toLowerCase().includes(searchValue) ||
|
||||
environments.some((env) => env.name.toLowerCase().includes(searchValue)) ||
|
||||
(secretPath ?? "*").toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
@@ -189,9 +192,18 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
.toLowerCase()
|
||||
.localeCompare(policyTwo.policyType.toLowerCase());
|
||||
case PolicyOrderBy.Environment:
|
||||
return policyOne.environment.name
|
||||
.toLowerCase()
|
||||
.localeCompare(policyTwo.environment.name.toLowerCase());
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const getFirstEnvName = (policy: { environments: { name: string }[] }) => {
|
||||
if (!policy.environments?.length) return "";
|
||||
return (
|
||||
policy.environments
|
||||
.map((env) => env.name?.toLowerCase() || "")
|
||||
.filter((name) => name)
|
||||
.sort()[0] || ""
|
||||
);
|
||||
};
|
||||
|
||||
return getFirstEnvName(policyOne).localeCompare(getFirstEnvName(policyTwo));
|
||||
case PolicyOrderBy.SecretPath:
|
||||
return (policyOne.secretPath ?? "*")
|
||||
.toLowerCase()
|
||||
|
@@ -54,7 +54,7 @@ type Props = {
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
environment: z.object({ slug: z.string(), name: z.string() }),
|
||||
environments: z.array(z.object({ slug: z.string(), name: z.string() })).min(1),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().trim().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
@@ -134,7 +134,7 @@ const Form = ({
|
||||
values: editValues
|
||||
? ({
|
||||
...editValues,
|
||||
environment: editValues.environment,
|
||||
environments: editValues.environments,
|
||||
userApprovers:
|
||||
editValues?.approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
@@ -191,7 +191,7 @@ const Form = ({
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: groups } = useListWorkspaceGroups(projectId);
|
||||
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const availableEnvironments = currentWorkspace?.environments || [];
|
||||
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
||||
|
||||
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
||||
@@ -204,11 +204,11 @@ const Form = ({
|
||||
|
||||
const formUserBypassers = watch("userBypassers");
|
||||
const formGroupBypassers = watch("groupBypassers");
|
||||
const formEnvironment = watch("environment")?.slug;
|
||||
const formEnvironments = watch("environments");
|
||||
const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length;
|
||||
|
||||
const handleCreatePolicy = async ({
|
||||
environment,
|
||||
environments,
|
||||
groupApprovers,
|
||||
userApprovers,
|
||||
groupBypassers,
|
||||
@@ -226,7 +226,7 @@ const Form = ({
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
environments: environments.map((env) => env.slug),
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
} else {
|
||||
@@ -242,7 +242,7 @@ const Form = ({
|
||||
numberOfApprovals: el.approvals
|
||||
})),
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
environments: environments.map((env) => env.slug),
|
||||
projectSlug
|
||||
});
|
||||
}
|
||||
@@ -261,7 +261,7 @@ const Form = ({
|
||||
};
|
||||
|
||||
const handleUpdatePolicy = async ({
|
||||
environment,
|
||||
environments,
|
||||
userApprovers,
|
||||
groupApprovers,
|
||||
userBypassers,
|
||||
@@ -281,7 +281,8 @@ const Form = ({
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
workspaceId: currentWorkspace?.id || "",
|
||||
environments: environments.map((env) => env.slug)
|
||||
});
|
||||
} else {
|
||||
await updateAccessApprovalPolicy({
|
||||
@@ -297,7 +298,7 @@ const Form = ({
|
||||
numberOfApprovals: el.approvals
|
||||
})),
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
environments: environments.map((env) => env.slug),
|
||||
projectSlug
|
||||
});
|
||||
}
|
||||
@@ -479,28 +480,28 @@ const Form = ({
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
environment={formEnvironment}
|
||||
environment={formEnvironments?.[0]?.slug || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
name="environments"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
label="Environments"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<FilterableSelect
|
||||
isDisabled={isEditMode}
|
||||
value={value}
|
||||
isMulti
|
||||
onChange={onChange}
|
||||
placeholder="Select environment..."
|
||||
options={environments}
|
||||
placeholder="Select environments..."
|
||||
options={availableEnvironments}
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
|
@@ -37,7 +37,7 @@ import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||
interface IPolicy {
|
||||
id: string;
|
||||
name: string;
|
||||
environment: WorkspaceEnv;
|
||||
environments: WorkspaceEnv[];
|
||||
projectId?: string;
|
||||
secretPath?: string;
|
||||
approvals: number;
|
||||
@@ -112,7 +112,7 @@ export const ApprovalPolicyRow = ({
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
>
|
||||
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td>
|
||||
<Td>{policy.environment.name}</Td>
|
||||
<Td>{policy.environments.map((env) => env.name).join(", ")}</Td>
|
||||
<Td>{policy.secretPath || "*"}</Td>
|
||||
<Td>
|
||||
<Badge
|
||||
|
@@ -975,12 +975,12 @@ const Page = () => {
|
||||
/>
|
||||
)}
|
||||
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
|
||||
{!canReadSecret &&
|
||||
!canReadDynamicSecret &&
|
||||
!canReadSecretImports &&
|
||||
folders?.length === 0 && <PermissionDeniedBanner />}
|
||||
</div>
|
||||
</div>
|
||||
{!canReadSecret &&
|
||||
!canReadDynamicSecret &&
|
||||
!canReadSecretImports &&
|
||||
folders?.length === 0 && <PermissionDeniedBanner />}
|
||||
{!isDetailsLoading &&
|
||||
(totalCount > 0 ||
|
||||
pendingChanges.secrets.length > 0 ||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { ContentLoader, EmptyState } from "@app/components/v2";
|
||||
import { AccessRestrictedBanner, ContentLoader, EmptyState } from "@app/components/v2";
|
||||
import { useSubscription, useWorkspace } from "@app/context";
|
||||
import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
@@ -16,16 +16,14 @@ export const ProjectScanningConfigTab = () => {
|
||||
|
||||
if (!subscription.secretScanning) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center px-20">
|
||||
<EmptyState
|
||||
className="rounded-md text-center"
|
||||
icon={faBan}
|
||||
title={
|
||||
<span>
|
||||
<div className="mt-60 flex h-full w-full items-center justify-center px-20">
|
||||
<AccessRestrictedBanner
|
||||
body={
|
||||
<>
|
||||
Your current plan doesn't support Secret Scanning.
|
||||
<br /> Please contact Infisical Support or reach out through our Slack channel for
|
||||
assistance.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
17
sink/oidc-server/package-lock.json
generated
17
sink/oidc-server/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.3",
|
||||
"axios": "^1.11.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.2",
|
||||
@@ -105,13 +105,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -571,14 +571,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": {
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.3",
|
||||
"axios": "^1.11.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.2",
|
||||
|
Reference in New Issue
Block a user