Allow multiple environments on secret and access policies

This commit is contained in:
Carlos Monastyrski
2025-07-23 09:54:41 -03:00
parent 0c855f3bd4
commit 05408bc151
31 changed files with 918 additions and 198 deletions
+21
View File
@@ -489,6 +489,11 @@ import {
TWorkflowIntegrationsInsert, TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate TWorkflowIntegrationsUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import {
TAccessApprovalPoliciesEnvironments,
TAccessApprovalPoliciesEnvironmentsInsert,
TAccessApprovalPoliciesEnvironmentsUpdate
} from "@app/db/schemas/access-approval-policies-environments";
import { import {
TIdentityLdapAuths, TIdentityLdapAuths,
TIdentityLdapAuthsInsert, TIdentityLdapAuthsInsert,
@@ -504,6 +509,11 @@ import {
TProjectMicrosoftTeamsConfigsInsert, TProjectMicrosoftTeamsConfigsInsert,
TProjectMicrosoftTeamsConfigsUpdate TProjectMicrosoftTeamsConfigsUpdate
} from "@app/db/schemas/project-microsoft-teams-configs"; } from "@app/db/schemas/project-microsoft-teams-configs";
import {
TSecretApprovalPoliciesEnvironments,
TSecretApprovalPoliciesEnvironmentsInsert,
TSecretApprovalPoliciesEnvironmentsUpdate
} from "@app/db/schemas/secret-approval-policies-environments";
import { import {
TSecretReminderRecipients, TSecretReminderRecipients,
TSecretReminderRecipientsInsert, TSecretReminderRecipientsInsert,
@@ -881,6 +891,12 @@ declare module "knex/types/tables" {
TAccessApprovalPoliciesBypassersUpdate TAccessApprovalPoliciesBypassersUpdate
>; >;
[TableName.AccessApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
TAccessApprovalPoliciesEnvironments,
TAccessApprovalPoliciesEnvironmentsInsert,
TAccessApprovalPoliciesEnvironmentsUpdate
>;
[TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType< [TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType<
TAccessApprovalRequests, TAccessApprovalRequests,
TAccessApprovalRequestsInsert, TAccessApprovalRequestsInsert,
@@ -929,6 +945,11 @@ declare module "knex/types/tables" {
TSecretApprovalRequestSecretTagsInsert, TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate TSecretApprovalRequestSecretTagsUpdate
>; >;
[TableName.SecretApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
TSecretApprovalPoliciesEnvironments,
TSecretApprovalPoliciesEnvironmentsInsert,
TSecretApprovalPoliciesEnvironmentsUpdate
>;
[TableName.SecretRotation]: KnexOriginal.CompositeTableType< [TableName.SecretRotation]: KnexOriginal.CompositeTableType<
TSecretRotations, TSecretRotations,
TSecretRotationsInsert, TSecretRotationsInsert,
@@ -0,0 +1,93 @@
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).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
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).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
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");
});
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment);
await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment);
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);
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);
}
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>
>;
+2
View File
@@ -100,6 +100,7 @@ export enum TableName {
AccessApprovalPolicyBypasser = "access_approval_policies_bypassers", AccessApprovalPolicyBypasser = "access_approval_policies_bypassers",
AccessApprovalRequest = "access_approval_requests", AccessApprovalRequest = "access_approval_requests",
AccessApprovalRequestReviewer = "access_approval_requests_reviewers", AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
AccessApprovalPolicyEnvironment = "access_approval_policies_environments",
SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers", SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers",
@@ -107,6 +108,7 @@ export enum TableName {
SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestReviewer = "secret_approval_requests_reviewers",
SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecret = "secret_approval_requests_secrets",
SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags", SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags",
SecretApprovalPolicyEnvironment = "secret_approval_policies_environments",
SecretRotation = "secret_rotations", SecretRotation = "secret_rotations",
SecretRotationOutput = "secret_rotation_outputs", SecretRotationOutput = "secret_rotation_outputs",
SamlConfig = "saml_configs", SamlConfig = "saml_configs",
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesEnvironmentsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretApprovalPoliciesEnvironments = z.infer<typeof SecretApprovalPoliciesEnvironmentsSchema>;
export type TSecretApprovalPoliciesEnvironmentsInsert = Omit<
z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>,
TImmutableDBKeys
>;
export type TSecretApprovalPoliciesEnvironmentsUpdate = Partial<
Omit<z.input<typeof SecretApprovalPoliciesEnvironmentsSchema>, TImmutableDBKeys>
>;
@@ -17,52 +17,66 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z
projectSlug: z.string().trim(), .object({
name: z.string().optional(), projectSlug: z.string().trim(),
secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash), name: z.string().optional(),
environment: z.string(), secretPath: z
approvers: z .string()
.discriminatedUnion("type", [ .trim()
z.object({ .min(1, { message: "Secret path cannot be empty" })
type: z.literal(ApproverType.Group), .transform(removeTrailingSlash),
id: z.string(), environment: z.string().optional(),
sequence: z.number().int().default(1) environments: z.string().array().optional(),
}), approvers: z
z.object({ .discriminatedUnion("type", [
type: z.literal(ApproverType.User), z.object({
id: z.string().optional(), type: z.literal(ApproverType.Group),
username: z.string().optional(), id: z.string(),
sequence: z.number().int().default(1) sequence: z.number().int().default(1)
}),
z.object({
type: z.literal(ApproverType.User),
id: z.string().optional(),
username: z.string().optional(),
sequence: z.number().int().default(1)
})
])
.array()
.max(100, "Cannot have more than 100 approvers")
.min(1, { message: "At least one approver should be provided" })
.refine(
// @ts-expect-error this is ok
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
"Must provide either username or id"
),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({
type: z.literal(BypasserType.User),
id: z.string().optional(),
username: z.string().optional()
})
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
}) })
]) .array()
.array() .optional(),
.max(100, "Cannot have more than 100 approvers") approvals: z.number().min(1).default(1),
.min(1, { message: "At least one approver should be provided" }) enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
.refine( allowedSelfApprovals: z.boolean().default(true)
// @ts-expect-error this is ok })
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)), .refine(
"Must provide either username or id" (val) => Boolean(val.environment) || Boolean(val.environments),
), "Must provide either environment or environments"
bypassers: z ),
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
})
.array()
.optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true)
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -78,7 +92,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectSlug: req.body.projectSlug, projectSlug: req.body.projectSlug,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, name: req.body.name ?? `${req.body.environment || req.body.environments?.join("-")}-${nanoid(3)}`,
enforcementLevel: req.body.enforcementLevel enforcementLevel: req.body.enforcementLevel
}); });
return { approval }; return { approval };
@@ -211,6 +225,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: z.number().min(1).optional(), approvals: z.number().min(1).optional(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true), allowedSelfApprovals: z.boolean().default(true),
environments: z.array(z.string()).optional(),
approvalsRequired: z approvalsRequired: z
.object({ .object({
numberOfApprovals: z.number().int(), numberOfApprovals: z.number().int(),
@@ -17,34 +17,45 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z
workspaceId: z.string(), .object({
name: z.string().optional(), workspaceId: z.string(),
environment: z.string(), name: z.string().optional(),
secretPath: z environment: z.string().optional(),
.string() environments: z.string().array().optional(),
.min(1, { message: "Secret path cannot be empty" }) secretPath: z
.transform((val) => removeTrailingSlash(val)), .string()
approvers: z .min(1, { message: "Secret path cannot be empty" })
.discriminatedUnion("type", [ .transform((val) => removeTrailingSlash(val)),
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), approvers: z
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() }) .discriminatedUnion("type", [
]) z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
.array() z.object({
.min(1, { message: "At least one approver should be provided" }) type: z.literal(ApproverType.User),
.max(100, "Cannot have more than 100 approvers"), id: z.string().optional(),
bypassers: z username: z.string().optional()
.discriminatedUnion("type", [ })
z.object({ type: z.literal(BypasserType.Group), id: z.string() }), ])
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() }) .array()
]) .min(1, { message: "At least one approver should be provided" })
.array() .max(100, "Cannot have more than 100 approvers"),
.max(100, "Cannot have more than 100 bypassers") bypassers: z
.optional(), .discriminatedUnion("type", [
approvals: z.number().min(1).default(1), z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), z.object({
allowedSelfApprovals: z.boolean().default(true) type: z.literal(BypasserType.User),
}), id: z.string().optional(),
username: z.string().optional()
})
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true)
})
.refine((data) => data.environment || data.environments, "At least one environment should be provided"),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -60,7 +71,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
...req.body, ...req.body,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`,
enforcementLevel: req.body.enforcementLevel enforcementLevel: req.body.enforcementLevel
}); });
return { approval }; return { approval };
@@ -103,7 +114,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.optional() .optional()
.transform((val) => (val ? removeTrailingSlash(val) : undefined)), .transform((val) => (val ? removeTrailingSlash(val) : undefined)),
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true),
environments: z.array(z.string()).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -26,6 +26,7 @@ export interface TAccessApprovalPolicyDALFactory
>, >,
customFilter?: { customFilter?: {
policyId?: string; policyId?: string;
envId?: string;
}, },
tx?: Knex tx?: Knex
) => Promise< ) => Promise<
@@ -55,11 +56,6 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath: string; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string; projectId: string;
bypassers: ( bypassers: (
| { | {
@@ -72,6 +68,11 @@ export interface TAccessApprovalPolicyDALFactory
type: BypasserType.Group; type: BypasserType.Group;
} }
)[]; )[];
environments: {
id: string;
name: string;
slug: string;
}[];
}[] }[]
>; >;
findById: ( findById: (
@@ -95,11 +96,11 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath: string; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environments: {
id: string; id: string;
name: string; name: string;
slug: string; slug: string;
}; }[];
projectId: string; projectId: string;
} }
| undefined | undefined
@@ -143,6 +144,26 @@ export interface TAccessApprovalPolicyDALFactory
} }
| undefined | undefined
>; >;
findPoliciesByEnvIdAndSecretPath: (
{ envIds, secretPath }: { envIds: string[]; secretPath: string },
tx?: Knex
) => Promise<{
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string;
}>;
} }
export interface TAccessApprovalPolicyServiceFactory { export interface TAccessApprovalPolicyServiceFactory {
@@ -367,6 +388,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
policyId?: string; policyId?: string;
envId?: string;
} }
) => { ) => {
const result = await tx(TableName.AccessApprovalPolicy) const result = await tx(TableName.AccessApprovalPolicy)
@@ -377,7 +399,17 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId); void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
} }
}) })
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .where((qb) => {
if (customFilter?.envId) {
void qb.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", customFilter.envId);
}
})
.join(
TableName.AccessApprovalPolicyEnvironment,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`
)
.join(TableName.Environment, `${TableName.AccessApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`)
.leftJoin( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
@@ -404,7 +436,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser)) .select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName")) .select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId")) .select(tx.ref("id").withSchema(TableName.Environment).as("environmentId"))
.select(tx.ref("projectId").withSchema(TableName.Environment)) .select(tx.ref("projectId").withSchema(TableName.Environment))
.select(selectAllTableCols(TableName.AccessApprovalPolicy)); .select(selectAllTableCols(TableName.AccessApprovalPolicy));
@@ -448,6 +480,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
sequence: approverSequence, sequence: approverSequence,
approvalsRequired approvalsRequired
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId: id, envName, envSlug }) => ({
id,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -470,11 +511,6 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
data: docs, data: docs,
key: "id", key: "id",
parentMapper: (data) => ({ parentMapper: (data) => ({
environment: {
id: data.envId,
name: data.envName,
slug: data.envSlug
},
projectId: data.projectId, projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data) ...AccessApprovalPoliciesSchema.parse(data)
// secretPath: data.secretPath || undefined, // secretPath: data.secretPath || undefined,
@@ -517,6 +553,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
id, id,
type: BypasserType.Group as const type: BypasserType.Group as const
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId: id, envName, envSlug }) => ({
id,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -545,14 +590,20 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
buildFindFilter( buildFindFilter(
{ {
envId,
secretPath secretPath
}, },
TableName.AccessApprovalPolicy TableName.AccessApprovalPolicy
) )
) )
.join(
TableName.AccessApprovalPolicyEnvironment,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", envId)
.orderBy("deletedAt", "desc") .orderBy("deletedAt", "desc")
.orderByRaw(`"deletedAt" IS NULL`) .orderByRaw(`"deletedAt" IS NULL`)
.select(selectAllTableCols(TableName.AccessApprovalPolicy))
.first(); .first();
return result; return result;
@@ -561,5 +612,81 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
} }
}; };
return { ...accessApprovalPolicyOrm, find, findById, softDeleteById, findLastValidPolicy }; const findPoliciesByEnvIdAndSecretPath: TAccessApprovalPolicyDALFactory["findPoliciesByEnvIdAndSecretPath"] = 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: "FindPoliciesByEnvIdAndSecretPath" });
}
};
return {
...accessApprovalPolicyOrm,
find,
findById,
softDeleteById,
findLastValidPolicy,
findPoliciesByEnvIdAndSecretPath
};
}; };
@@ -0,0 +1,31 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TAccessApprovalPolicyEnvironmentDALFactory = ReturnType<typeof accessApprovalPolicyEnvironmentDALFactory>;
export const accessApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
const accessApprovalPolicyEnvironmentOrm = ormify(db, TableName.AccessApprovalPolicyEnvironment);
const findAvailablePoliciesIds = async (envId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicyEnvironment)
.join(
TableName.AccessApprovalPolicy,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`,
`${TableName.AccessApprovalPolicy}.id`
)
.where({ [`${TableName.AccessApprovalPolicyEnvironment}.envId` as "envId"]: envId })
.whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`)
.select(selectAllTableCols(TableName.AccessApprovalPolicyEnvironment));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "findAvailablePoliciesIds" });
}
};
return { ...accessApprovalPolicyEnvironmentOrm, findAvailablePoliciesIds };
};
@@ -20,6 +20,7 @@ import {
TAccessApprovalPolicyBypasserDALFactory TAccessApprovalPolicyBypasserDALFactory
} from "./access-approval-policy-approver-dal"; } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { TAccessApprovalPolicyEnvironmentDALFactory } from "./access-approval-policy-environment-dal";
import { import {
ApproverType, ApproverType,
BypasserType, BypasserType,
@@ -44,12 +45,14 @@ type TAccessApprovalPolicyServiceFactoryDep = {
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">; accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
accessApprovalPolicyEnvironmentDAL: TAccessApprovalPolicyEnvironmentDALFactory;
}; };
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL, accessApprovalPolicyBypasserDAL,
accessApprovalPolicyEnvironmentDAL,
groupDAL, groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
@@ -62,21 +65,22 @@ export const accessApprovalPolicyServiceFactory = ({
}: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => {
const $policyExists = async ({ const $policyExists = async ({
envId, envId,
envIds,
secretPath, secretPath,
policyId policyId
}: { }: {
envId: string; envId?: string;
envIds?: string[];
secretPath: string; secretPath: string;
policyId?: string; policyId?: string;
}) => { }) => {
const policy = await accessApprovalPolicyDAL if (!envId && !envIds) {
.findOne({ throw new BadRequestError({ message: "Must provide either envId or envIds" });
envId, }
secretPath, const policy = await accessApprovalPolicyDAL.findPoliciesByEnvIdAndSecretPath({
deletedAt: null secretPath,
}) envIds: envId ? [envId] : envIds || []
.catch(() => null); });
return policyId ? policy && policy.id !== policyId : Boolean(policy); return policyId ? policy && policy.id !== policyId : Boolean(policy);
}; };
@@ -92,6 +96,7 @@ export const accessApprovalPolicyServiceFactory = ({
bypassers, bypassers,
projectSlug, projectSlug,
environment, environment,
environments,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired
@@ -123,13 +128,23 @@ export const accessApprovalPolicyServiceFactory = ({
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const mergedEnvs = (environment ? [environment] : environments) || [];
if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); if (mergedEnvs.length === 0) {
throw new BadRequestError({ message: "Must provide either environment or environments" });
}
const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId: project.id });
if (!envs.length || envs.length !== mergedEnvs.length) {
const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env));
throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` });
}
if (await $policyExists({ envId: env.id, secretPath })) { for (const env of envs) {
throw new BadRequestError({ // eslint-disable-next-line no-await-in-loop
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` if (await $policyExists({ envId: env.id, secretPath })) {
}); throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'`
});
}
} }
let approverUserIds = userApprovers; let approverUserIds = userApprovers;
@@ -197,7 +212,7 @@ export const accessApprovalPolicyServiceFactory = ({
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create( const doc = await accessApprovalPolicyDAL.create(
{ {
envId: env.id, envId: envs[0].id,
approvals, approvals,
secretPath, secretPath,
name, name,
@@ -206,6 +221,10 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await accessApprovalPolicyEnvironmentDAL.insertMany(
envs.map((el) => ({ policyId: doc.id, envId: el.id })),
tx
);
if (approverUserIds.length) { if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
@@ -258,7 +277,7 @@ export const accessApprovalPolicyServiceFactory = ({
return doc; return doc;
}); });
return { ...accessApproval, environment: env, projectId: project.id }; return { ...accessApproval, environments: envs, projectId: project.id, environment: envs[0] };
}; };
const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] = const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] =
@@ -276,7 +295,10 @@ export const accessApprovalPolicyServiceFactory = ({
}); });
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
return accessApprovalPolicies; return accessApprovalPolicies.map((policy) => ({
...policy,
environment: policy.environments[0]
}));
}; };
const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({ const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({
@@ -292,7 +314,8 @@ export const accessApprovalPolicyServiceFactory = ({
approvals, approvals,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}: TUpdateAccessApprovalPolicy) => { }: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group); const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
@@ -320,15 +343,23 @@ export const accessApprovalPolicyServiceFactory = ({
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
} }
let envs = accessApprovalPolicy.environments;
if (
environments &&
(environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
) {
envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: accessApprovalPolicy.projectId });
}
if ( if (
await $policyExists({ await $policyExists({
envId: accessApprovalPolicy.envId, envIds: envs.map((env) => env.id),
secretPath: secretPath || accessApprovalPolicy.secretPath, secretPath: secretPath || accessApprovalPolicy.secretPath,
policyId: accessApprovalPolicy.id policyId: accessApprovalPolicy.id
}) })
) { ) {
throw new BadRequestError({ throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'` message: `A policy for secret path '${secretPath}' already exists`
}); });
} }
@@ -484,6 +515,14 @@ export const accessApprovalPolicyServiceFactory = ({
); );
} }
if (environments) {
await accessApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyEnvironmentDAL.insertMany(
envs.map((env) => ({ policyId: doc.id, envId: env.id })),
tx
);
}
await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx); await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) { if (bypasserUserIds.length) {
@@ -513,7 +552,8 @@ export const accessApprovalPolicyServiceFactory = ({
return { return {
...updatedPolicy, ...updatedPolicy,
environment: accessApprovalPolicy.environment, environments: accessApprovalPolicy.environments,
environment: accessApprovalPolicy.environments[0],
projectId: accessApprovalPolicy.projectId projectId: accessApprovalPolicy.projectId
}; };
}; };
@@ -563,7 +603,10 @@ export const accessApprovalPolicyServiceFactory = ({
} }
}); });
return policy; return {
...policy,
environment: policy.environments[0]
};
}; };
const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({ const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({
@@ -592,11 +635,13 @@ export const accessApprovalPolicyServiceFactory = ({
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` }); if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
const policies = await accessApprovalPolicyDAL.find({ const policies = await accessApprovalPolicyDAL.find(
envId: environment.id, {
projectId: project.id, projectId: project.id,
deletedAt: null deletedAt: null
}); },
{ envId: environment.id }
);
if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` }); if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` });
return { count: policies.length }; return { count: policies.length };
@@ -627,7 +672,10 @@ export const accessApprovalPolicyServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return policy; return {
...policy,
environment: policy.environments[0]
};
}; };
return { return {
@@ -26,7 +26,8 @@ export enum BypasserType {
export type TCreateAccessApprovalPolicy = { export type TCreateAccessApprovalPolicy = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment?: string;
environments?: string[];
approvers: ( approvers: (
| { type: ApproverType.Group; id: string; sequence?: number } | { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number } | { type: ApproverType.User; id?: string; username?: string; sequence?: number }
@@ -58,6 +59,7 @@ export type TUpdateAccessApprovalPolicy = {
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
environments?: string[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteAccessApprovalPolicy = { export type TDeleteAccessApprovalPolicy = {
@@ -113,6 +115,15 @@ export interface TAccessApprovalPolicyServiceFactory {
slug: string; slug: string;
position: number; position: number;
}; };
environments: {
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
projectId: string;
slug: string;
position: number;
}[];
projectId: string; projectId: string;
name: string; name: string;
id: string; id: string;
@@ -153,6 +164,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
}>; }>;
updateAccessApprovalPolicy: ({ updateAccessApprovalPolicy: ({
@@ -168,13 +184,19 @@ export interface TAccessApprovalPolicyServiceFactory {
approvals, approvals,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}: TUpdateAccessApprovalPolicy) => Promise<{ }: TUpdateAccessApprovalPolicy) => Promise<{
environment: { environment: {
id: string; id: string;
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
name: string; name: string;
id: string; id: string;
@@ -225,6 +247,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
bypassers: ( bypassers: (
| { | {
@@ -276,6 +303,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string; name: string;
slug: string; slug: string;
}; };
environments: {
id: string;
name: string;
slug: string;
}[];
projectId: string; projectId: string;
bypassers: ( bypassers: (
| { | {
@@ -65,7 +65,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
deletedAt: Date | null | undefined; deletedAt: Date | null | undefined;
}; };
projectId: string; projectId: string;
environment: string; environments: string[];
requestedByUser: { requestedByUser: {
userId: string; userId: string;
email: string | null | undefined; email: string | null | undefined;
@@ -515,7 +515,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
`accessApprovalReviewerUser.id` `accessApprovalReviewerUser.id`
) )
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .leftJoin(
TableName.AccessApprovalPolicyEnvironment,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyEnvironment}.policyId`
)
.leftJoin(
TableName.Environment,
`${TableName.AccessApprovalPolicyEnvironment}.envId`,
`${TableName.Environment}.id`
)
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
@@ -683,6 +693,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
lastName, lastName,
username username
}) })
},
{
key: "environment",
label: "environments" as const,
mapper: ({ environment }) => environment
} }
] ]
}); });
@@ -86,6 +86,25 @@ export const accessApprovalRequestServiceFactory = ({
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
projectSlackConfigDAL projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => { }: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => {
const $getEnvironmentFromPermissions = (permissions: unknown): string | null => {
if (!Array.isArray(permissions) || permissions.length === 0) {
return null;
}
const firstPermission = permissions[0] as unknown[];
if (!Array.isArray(firstPermission) || firstPermission.length < 3) {
return null;
}
const metadata = firstPermission[2] as Record<string, unknown>;
if (typeof metadata === "object" && metadata !== null && "environment" in metadata) {
const env = metadata.environment;
return typeof env === "string" ? env : null;
}
return null;
};
const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({ const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({
isTemporary, isTemporary,
temporaryRange, temporaryRange,
@@ -323,13 +342,27 @@ export const accessApprovalRequestServiceFactory = ({
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` }); throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
} }
const { policy, environment } = accessApprovalRequest; const { policy, environments, permissions } = accessApprovalRequest;
if (policy.deletedAt) { if (policy.deletedAt) {
throw new BadRequestError({ throw new BadRequestError({
message: "The policy associated with this access request has been deleted." message: "The policy associated with this access request has been deleted."
}); });
} }
const permissionEnvironment = $getEnvironmentFromPermissions(permissions);
if (
!permissionEnvironment ||
(!environments.includes(permissionEnvironment) && status === ApprovalStatus.APPROVED)
) {
throw new BadRequestError({
message: `The original policy ${policy.name} is not attached to environment '${permissionEnvironment}'.`
});
}
const environment = await projectEnvDAL.findOne({
projectId: accessApprovalRequest.projectId,
slug: permissionEnvironment
});
const { membership, hasRole } = await permissionService.getProjectPermission({ const { membership, hasRole } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -550,7 +583,7 @@ export const accessApprovalRequestServiceFactory = ({
requesterEmail: actingUser.email, requesterEmail: actingUser.email,
bypassReason: bypassReason || "No reason provided", bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/", secretPath: policy.secretPath || "/",
environment, environment: environment?.name || permissionEnvironment,
approvalUrl: `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval`, approvalUrl: `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval`,
requestType: "access" requestType: "access"
}, },
@@ -23,6 +23,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
sapId?: string; sapId?: string;
envId?: string;
} }
) => ) =>
tx(TableName.SecretApprovalPolicy) tx(TableName.SecretApprovalPolicy)
@@ -33,7 +34,17 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId); void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
} }
}) })
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`) .join(
TableName.SecretApprovalPolicyEnvironment,
`${TableName.SecretApprovalPolicyEnvironment}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.join(TableName.Environment, `${TableName.SecretApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`)
.where((qb) => {
if (customFilter?.envId) {
void qb.where(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", customFilter.envId);
}
})
.leftJoin( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
@@ -97,7 +108,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
.select( .select(
tx.ref("name").withSchema(TableName.Environment).as("envName"), tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"), tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
tx.ref("id").withSchema(TableName.Environment).as("envId"), tx.ref("id").withSchema(TableName.Environment).as("environmentId"),
tx.ref("projectId").withSchema(TableName.Environment) tx.ref("projectId").withSchema(TableName.Environment)
) )
.select(selectAllTableCols(TableName.SecretApprovalPolicy)) .select(selectAllTableCols(TableName.SecretApprovalPolicy))
@@ -146,6 +157,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
firstName, firstName,
lastName lastName
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId, envName, envSlug }) => ({
id: environmentId,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -160,6 +180,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
sapId?: string; sapId?: string;
envId?: string;
}, },
tx?: Knex tx?: Knex
) => { ) => {
@@ -221,6 +242,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
mapper: ({ approverGroupUserId: userId }) => ({ mapper: ({ approverGroupUserId: userId }) => ({
userId userId
}) })
},
{
key: "environmentId",
label: "environments" as const,
mapper: ({ environmentId, envName, envSlug }) => ({
id: environmentId,
name: envName,
slug: envSlug
})
} }
] ]
}); });
@@ -235,5 +265,74 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
return softDeletedPolicy; return softDeletedPolicy;
}; };
return { ...secretApprovalPolicyOrm, findById, find, softDeleteById }; const findPoliciesByEnvIdAndSecretPath = 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: "FindPoliciesByEnvIdAndSecretPath" });
}
};
return { ...secretApprovalPolicyOrm, findById, find, softDeleteById, findPoliciesByEnvIdAndSecretPath };
}; };
@@ -0,0 +1,31 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretApprovalPolicyEnvironmentDALFactory = ReturnType<typeof secretApprovalPolicyEnvironmentDALFactory>;
export const secretApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
const secretApprovalPolicyEnvironmentOrm = ormify(db, TableName.SecretApprovalPolicyEnvironment);
const findAvailablePoliciesIds = async (envId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicyEnvironment)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicyEnvironment}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.where({ [`${TableName.SecretApprovalPolicyEnvironment}.envId` as "envId"]: envId })
.whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`)
.select(selectAllTableCols(TableName.SecretApprovalPolicyEnvironment));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "findAvailablePoliciesIds" });
}
};
return { ...secretApprovalPolicyEnvironmentOrm, findAvailablePoliciesIds };
};
@@ -18,6 +18,7 @@ import {
TSecretApprovalPolicyBypasserDALFactory TSecretApprovalPolicyBypasserDALFactory
} from "./secret-approval-policy-approver-dal"; } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import { TSecretApprovalPolicyEnvironmentDALFactory } from "./secret-approval-policy-environment-dal";
import { import {
TCreateSapDTO, TCreateSapDTO,
TDeleteSapDTO, TDeleteSapDTO,
@@ -35,12 +36,13 @@ const getPolicyScore = (policy: { secretPath?: string | null }) =>
type TSecretApprovalPolicyServiceFactoryDep = { type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
userDAL: Pick<TUserDALFactory, "find">; userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory; secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">; secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">;
secretApprovalPolicyEnvironmentDAL: TSecretApprovalPolicyEnvironmentDALFactory;
}; };
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>; export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@@ -50,27 +52,30 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService, permissionService,
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
secretApprovalPolicyBypasserDAL, secretApprovalPolicyBypasserDAL,
secretApprovalPolicyEnvironmentDAL,
projectEnvDAL, projectEnvDAL,
userDAL, userDAL,
licenseService, licenseService,
secretApprovalRequestDAL secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const $policyExists = async ({ const $policyExists = async ({
envIds,
envId, envId,
secretPath, secretPath,
policyId policyId
}: { }: {
envId: string; envIds?: string[];
envId?: string;
secretPath: string; secretPath: string;
policyId?: string; policyId?: string;
}) => { }) => {
const policy = await secretApprovalPolicyDAL if (!envIds && !envId) {
.findOne({ throw new BadRequestError({ message: "At least one environment should be provided" });
envId, }
secretPath, const policy = await secretApprovalPolicyDAL.findPoliciesByEnvIdAndSecretPath({
deletedAt: null envIds: envId ? [envId] : envIds || [],
}) secretPath
.catch(() => null); });
return policyId ? policy && policy.id !== policyId : Boolean(policy); return policyId ? policy && policy.id !== policyId : Boolean(policy);
}; };
@@ -87,6 +92,7 @@ export const secretApprovalPolicyServiceFactory = ({
projectId, projectId,
secretPath, secretPath,
environment, environment,
environments,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals
}: TCreateSapDTO) => { }: TCreateSapDTO) => {
@@ -125,17 +131,23 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
const env = await projectEnvDAL.findOne({ slug: environment, projectId }); const mergedEnvs = (environment ? [environment] : environments) || [];
if (!env) { if (mergedEnvs.length === 0) {
throw new NotFoundError({ throw new BadRequestError({ message: "Must provide either environment or environments" });
message: `Environment with slug '${environment}' not found in project with ID ${projectId}` }
}); const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId });
if (!envs.length || envs.length !== mergedEnvs.length) {
const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env));
throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` });
} }
if (await $policyExists({ envId: env.id, secretPath })) { for (const env of envs) {
throw new BadRequestError({ // eslint-disable-next-line no-await-in-loop
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` if (await $policyExists({ envId: env.id, secretPath })) {
}); throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'`
});
}
} }
let groupBypassers: string[] = []; let groupBypassers: string[] = [];
@@ -179,7 +191,7 @@ export const secretApprovalPolicyServiceFactory = ({
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => { const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.create( const doc = await secretApprovalPolicyDAL.create(
{ {
envId: env.id, envId: envs[0].id,
approvals, approvals,
secretPath, secretPath,
name, name,
@@ -188,6 +200,13 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await secretApprovalPolicyEnvironmentDAL.insertMany(
envs.map((env) => ({
envId: env.id,
policyId: doc.id
})),
tx
);
let userApproverIds = userApprovers; let userApproverIds = userApprovers;
if (userApproverNames.length) { if (userApproverNames.length) {
@@ -251,12 +270,13 @@ export const secretApprovalPolicyServiceFactory = ({
return doc; return doc;
}); });
return { ...secretApproval, environment: env, projectId }; return { ...secretApproval, environments: envs, projectId, environment: envs[0] };
}; };
const updateSecretApprovalPolicy = async ({ const updateSecretApprovalPolicy = async ({
approvers, approvers,
bypassers, bypassers,
environments,
secretPath, secretPath,
name, name,
actorId, actorId,
@@ -286,16 +306,22 @@ export const secretApprovalPolicyServiceFactory = ({
message: `Secret approval policy with ID '${secretPolicyId}' not found` message: `Secret approval policy with ID '${secretPolicyId}' not found`
}); });
} }
let envs = secretApprovalPolicy.environments;
if (
environments &&
(environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
) {
envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: secretApprovalPolicy.projectId });
}
if ( if (
await $policyExists({ await $policyExists({
envId: secretApprovalPolicy.envId, envIds: envs.map((env) => env.id),
secretPath: secretPath || secretApprovalPolicy.secretPath, secretPath: secretPath || secretApprovalPolicy.secretPath,
policyId: secretApprovalPolicy.id policyId: secretApprovalPolicy.id
}) })
) { ) {
throw new BadRequestError({ throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'` message: `A policy for secret path '${secretPath}' already exists`
}); });
} }
@@ -412,6 +438,17 @@ export const secretApprovalPolicyServiceFactory = ({
); );
} }
if (environments) {
await secretApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx);
await secretApprovalPolicyEnvironmentDAL.insertMany(
envs.map((env) => ({
envId: env.id,
policyId: doc.id
})),
tx
);
}
await secretApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx); await secretApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) { if (bypasserUserIds.length) {
@@ -438,7 +475,8 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
return { return {
...updatedSap, ...updatedSap,
environment: secretApprovalPolicy.environment, environments: secretApprovalPolicy.environments,
environment: secretApprovalPolicy.environments[0],
projectId: secretApprovalPolicy.projectId projectId: secretApprovalPolicy.projectId
}; };
}; };
@@ -483,7 +521,12 @@ export const secretApprovalPolicyServiceFactory = ({
const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx); const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx);
return updatedPolicy; return updatedPolicy;
}); });
return { ...deletedPolicy, projectId: sapPolicy.projectId, environment: sapPolicy.environment }; return {
...deletedPolicy,
projectId: sapPolicy.projectId,
environments: sapPolicy.environments,
environment: sapPolicy.environments[0]
};
}; };
const getSecretApprovalPolicyByProjectId = async ({ const getSecretApprovalPolicyByProjectId = async ({
@@ -515,7 +558,7 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
const policies = await secretApprovalPolicyDAL.find({ envId: env.id, deletedAt: null }); const policies = await secretApprovalPolicyDAL.find({ deletedAt: null }, { envId: env.id });
if (!policies.length) return; if (!policies.length) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path // this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter( const policiesFilteredByPath = policies.filter(
@@ -5,7 +5,8 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app
export type TCreateSapDTO = { export type TCreateSapDTO = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment?: string;
environments?: string[];
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: ( bypassers?: (
| { type: BypasserType.Group; id: string } | { type: BypasserType.Group; id: string }
@@ -29,6 +30,7 @@ export type TUpdateSapDTO = {
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals?: boolean; allowedSelfApprovals?: boolean;
environments?: string[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteSapDTO = { export type TDeleteSapDTO = {
@@ -40,6 +40,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.leftJoin(TableName.SecretApprovalPolicyEnvironment, (bd) => {
bd.on(
`${TableName.SecretApprovalPolicy}.id`,
"=",
`${TableName.SecretApprovalPolicyEnvironment}.policyId`
).andOn(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", `${TableName.SecretFolder}.envId`);
})
.leftJoin<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("statusChangedByUser"), db(TableName.Users).as("statusChangedByUser"),
`${TableName.SecretApprovalRequest}.statusChangedByUserId`, `${TableName.SecretApprovalRequest}.statusChangedByUserId`,
@@ -146,7 +153,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment), tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), tx.ref("envId").withSchema(TableName.SecretApprovalPolicyEnvironment).as("policyEnvId"),
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
@@ -531,6 +531,11 @@ export const secretApprovalRequestServiceFactory = ({
message: "The policy associated with this secret approval request has been deleted." message: "The policy associated with this secret approval request has been deleted."
}); });
} }
if (!policy.envId) {
throw new BadRequestError({
message: "The policy associated with this secret approval request is not linked to the environment."
});
}
const { hasRole } = await permissionService.getProjectPermission({ const { hasRole } = await permissionService.getProjectPermission({
actor: ActorType.USER, actor: ActorType.USER,
+9 -1
View File
@@ -11,6 +11,7 @@ import {
accessApprovalPolicyBypasserDALFactory accessApprovalPolicyBypasserDALFactory
} from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
import { accessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal";
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal"; import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal"; import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
@@ -76,6 +77,7 @@ import {
secretApprovalPolicyBypasserDALFactory secretApprovalPolicyBypasserDALFactory
} from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal"; } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal"; import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
import { secretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal";
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal"; import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal";
@@ -418,9 +420,11 @@ export const registerRoutes = async (
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db); const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db);
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db); const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
const accessApprovalPolicyEnvironmentDAL = accessApprovalPolicyEnvironmentDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db); const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db);
const sapEnvironmentDAL = secretApprovalPolicyEnvironmentDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db); const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
@@ -554,6 +558,7 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL, secretApprovalPolicyApproverDAL: sapApproverDAL,
secretApprovalPolicyBypasserDAL: sapBypasserDAL, secretApprovalPolicyBypasserDAL: sapBypasserDAL,
secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL,
permissionService, permissionService,
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
licenseService, licenseService,
@@ -1141,7 +1146,9 @@ export const registerRoutes = async (
keyStore, keyStore,
licenseService, licenseService,
projectDAL, projectDAL,
folderDAL folderDAL,
accessApprovalPolicyEnvironmentDAL,
secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL
}); });
const projectRoleService = projectRoleServiceFactory({ const projectRoleService = projectRoleServiceFactory({
@@ -1300,6 +1307,7 @@ export const registerRoutes = async (
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL, accessApprovalPolicyBypasserDAL,
accessApprovalPolicyEnvironmentDAL,
groupDAL, groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
@@ -93,6 +93,13 @@ export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
name: z.string(), name: z.string(),
slug: z.string() slug: z.string()
}), }),
environments: z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string()
})
),
projectId: z.string() projectId: z.string()
}) })
); );
@@ -1,8 +1,10 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { TAccessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -19,6 +21,8 @@ type TProjectEnvServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
accessApprovalPolicyEnvironmentDAL: Pick<TAccessApprovalPolicyEnvironmentDALFactory, "findAvailablePoliciesIds">;
secretApprovalPolicyEnvironmentDAL: Pick<TSecretApprovalPolicyEnvironmentDALFactory, "findAvailablePoliciesIds">;
}; };
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>; export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
@@ -29,7 +33,9 @@ export const projectEnvServiceFactory = ({
licenseService, licenseService,
keyStore, keyStore,
projectDAL, projectDAL,
folderDAL folderDAL,
accessApprovalPolicyEnvironmentDAL,
secretApprovalPolicyEnvironmentDAL
}: TProjectEnvServiceFactoryDep) => { }: TProjectEnvServiceFactoryDep) => {
const createEnvironment = async ({ const createEnvironment = async ({
projectId, projectId,
@@ -216,6 +222,20 @@ export const projectEnvServiceFactory = ({
} }
const env = await projectEnvDAL.transaction(async (tx) => { const env = await projectEnvDAL.transaction(async (tx) => {
const secretApprovalRequest = await secretApprovalPolicyEnvironmentDAL.findAvailablePoliciesIds(id, tx);
if (secretApprovalRequest.length > 0) {
throw new BadRequestError({
message: "Environment is in use by a secret approval policy",
name: "DeleteEnvironment"
});
}
const accessApprovalPolicy = await accessApprovalPolicyEnvironmentDAL.findAvailablePoliciesIds(id, tx);
if (accessApprovalPolicy.length > 0) {
throw new BadRequestError({
message: "Environment is in use by an access approval policy",
name: "DeleteEnvironment"
});
}
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx); const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
if (!doc) if (!doc)
throw new NotFoundError({ throw new NotFoundError({
@@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => {
return useMutation<object, object, TCreateAccessPolicyDTO>({ return useMutation<object, object, TCreateAccessPolicyDTO>({
mutationFn: async ({ mutationFn: async ({
environment, environments,
projectSlug, projectSlug,
approvals, approvals,
approvers, approvers,
@@ -29,7 +29,7 @@ export const useCreateAccessApprovalPolicy = () => {
approvalsRequired approvalsRequired
}) => { }) => {
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
environment, environments,
projectSlug, projectSlug,
approvals, approvals,
bypassers, bypassers,
@@ -63,7 +63,8 @@ export const useUpdateAccessApprovalPolicy = () => {
secretPath, secretPath,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}) => { }) => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals, approvals,
@@ -73,7 +74,8 @@ export const useUpdateAccessApprovalPolicy = () => {
name, name,
enforcementLevel, enforcementLevel,
allowedSelfApprovals, allowedSelfApprovals,
approvalsRequired approvalsRequired,
environments
}); });
return data; return data;
}, },
@@ -10,7 +10,7 @@ export type TAccessApprovalPolicy = {
secretPath: string; secretPath: string;
envId: string; envId: string;
workspace: string; workspace: string;
environment: WorkspaceEnv; environments: WorkspaceEnv[];
projectId: string; projectId: string;
policyType: PolicyType; policyType: PolicyType;
approversRequired: boolean; approversRequired: boolean;
@@ -166,7 +166,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
export type TCreateAccessPolicyDTO = { export type TCreateAccessPolicyDTO = {
projectSlug: string; projectSlug: string;
name?: string; name?: string;
environment: string; environments: string[];
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
approvals?: number; approvals?: number;
@@ -182,7 +182,7 @@ export type TUpdateAccessPolicyDTO = {
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
secretPath?: string; secretPath?: string;
environment?: string; environments?: string[];
approvals?: number; approvals?: number;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
@@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => {
return useMutation<object, object, TCreateSecretPolicyDTO>({ return useMutation<object, object, TCreateSecretPolicyDTO>({
mutationFn: async ({ mutationFn: async ({
environment, environments,
workspaceId, workspaceId,
approvals, approvals,
approvers, approvers,
@@ -21,7 +21,7 @@ export const useCreateSecretApprovalPolicy = () => {
allowedSelfApprovals allowedSelfApprovals
}) => { }) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", { const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment, environments,
workspaceId, workspaceId,
approvals, approvals,
approvers, approvers,
@@ -53,7 +53,8 @@ export const useUpdateSecretApprovalPolicy = () => {
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals,
environments
}) => { }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals, approvals,
@@ -62,7 +63,8 @@ export const useUpdateSecretApprovalPolicy = () => {
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals,
environments
}); });
return data; return data;
}, },
@@ -6,7 +6,7 @@ export type TSecretApprovalPolicy = {
workspace: string; workspace: string;
name: string; name: string;
envId: string; envId: string;
environment: WorkspaceEnv; environments: WorkspaceEnv[];
secretPath?: string; secretPath?: string;
approvals: number; approvals: number;
approvers: Approver[]; approvers: Approver[];
@@ -48,7 +48,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
export type TCreateSecretPolicyDTO = { export type TCreateSecretPolicyDTO = {
workspaceId: string; workspaceId: string;
name?: string; name?: string;
environment: string; environments: string[];
secretPath: string; secretPath: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
@@ -68,6 +68,7 @@ export type TUpdateSecretPolicyDTO = {
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
// for invalidating list // for invalidating list
workspaceId: string; workspaceId: string;
environments?: string[];
}; };
export type TDeleteSecretPolicyDTO = { export type TDeleteSecretPolicyDTO = {
+2 -1
View File
@@ -49,7 +49,8 @@ export const usePathAccessPolicies = ({ secretPath, environment }: Params) => {
return useMemo(() => { return useMemo(() => {
const pathPolicies = policies?.filter( const pathPolicies = policies?.filter(
(policy) => (policy) =>
policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath) policy.environments?.some((env) => env.slug === environment) &&
matchesPath(secretPath, policy.secretPath)
); );
return { return {
@@ -155,8 +155,8 @@ export const SpecificPrivilegeSecretForm = ({
const selectablePaths = useMemo(() => { const selectablePaths = useMemo(() => {
if (!policies) return []; if (!policies) return [];
const environmentPolicies = policies.filter( const environmentPolicies = policies.filter((policy) =>
(policy) => policy.environment.slug === selectedEnvironment policy.environments.find((env) => env.slug === selectedEnvironment)
); );
privilegeForm.setValue("secretPath", "", { privilegeForm.setValue("secretPath", "", {
@@ -166,17 +166,20 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
const filteredPolicies = useMemo( const filteredPolicies = useMemo(
() => () =>
policies policies
.filter(({ policyType, environment, name, secretPath }) => { .filter(({ policyType, environments, name, secretPath }) => {
if (filters.type && policyType !== filters.type) return false; if (filters.type && policyType !== filters.type) return false;
if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id)) if (
filters.environmentIds.length &&
!environments.some((env) => filters.environmentIds.includes(env.id))
)
return false; return false;
const searchValue = search.trim().toLowerCase(); const searchValue = search.trim().toLowerCase();
return ( return (
name.toLowerCase().includes(searchValue) || name.toLowerCase().includes(searchValue) ||
environment.name.toLowerCase().includes(searchValue) || environments.some((env) => env.name.toLowerCase().includes(searchValue)) ||
(secretPath ?? "*").toLowerCase().includes(searchValue) (secretPath ?? "*").toLowerCase().includes(searchValue)
); );
}) })
@@ -189,9 +192,18 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
.toLowerCase() .toLowerCase()
.localeCompare(policyTwo.policyType.toLowerCase()); .localeCompare(policyTwo.policyType.toLowerCase());
case PolicyOrderBy.Environment: case PolicyOrderBy.Environment:
return policyOne.environment.name // eslint-disable-next-line no-case-declarations
.toLowerCase() const getFirstEnvName = (policy: { environments: { name: string }[] }) => {
.localeCompare(policyTwo.environment.name.toLowerCase()); if (!policy.environments?.length) return "";
return (
policy.environments
.map((env) => env.name?.toLowerCase() || "")
.filter((name) => name)
.sort()[0] || ""
);
};
return getFirstEnvName(policyOne).localeCompare(getFirstEnvName(policyTwo));
case PolicyOrderBy.SecretPath: case PolicyOrderBy.SecretPath:
return (policyOne.secretPath ?? "*") return (policyOne.secretPath ?? "*")
.toLowerCase() .toLowerCase()
@@ -54,7 +54,7 @@ type Props = {
const formSchema = z const formSchema = z
.object({ .object({
environment: z.object({ slug: z.string(), name: z.string() }), environments: z.array(z.object({ slug: z.string(), name: z.string() })).min(1),
name: z.string().optional(), name: z.string().optional(),
secretPath: z.string().trim().min(1), secretPath: z.string().trim().min(1),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
@@ -134,7 +134,7 @@ const Form = ({
values: editValues values: editValues
? ({ ? ({
...editValues, ...editValues,
environment: editValues.environment, environments: editValues.environments,
userApprovers: userApprovers:
editValues?.approvers editValues?.approvers
?.filter((approver) => approver.type === ApproverType.User) ?.filter((approver) => approver.type === ApproverType.User)
@@ -191,7 +191,7 @@ const Form = ({
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: groups } = useListWorkspaceGroups(projectId); const { data: groups } = useListWorkspaceGroups(projectId);
const environments = currentWorkspace?.environments || []; const availableEnvironments = currentWorkspace?.environments || [];
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy; const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
@@ -204,11 +204,11 @@ const Form = ({
const formUserBypassers = watch("userBypassers"); const formUserBypassers = watch("userBypassers");
const formGroupBypassers = watch("groupBypassers"); const formGroupBypassers = watch("groupBypassers");
const formEnvironment = watch("environment")?.slug; const formEnvironments = watch("environments");
const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length; const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length;
const handleCreatePolicy = async ({ const handleCreatePolicy = async ({
environment, environments,
groupApprovers, groupApprovers,
userApprovers, userApprovers,
groupBypassers, groupBypassers,
@@ -226,7 +226,7 @@ const Form = ({
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environments: environments.map((env) => env.slug),
workspaceId: currentWorkspace?.id || "" workspaceId: currentWorkspace?.id || ""
}); });
} else { } else {
@@ -242,7 +242,7 @@ const Form = ({
numberOfApprovals: el.approvals numberOfApprovals: el.approvals
})), })),
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environments: environments.map((env) => env.slug),
projectSlug projectSlug
}); });
} }
@@ -261,7 +261,7 @@ const Form = ({
}; };
const handleUpdatePolicy = async ({ const handleUpdatePolicy = async ({
environment, environments,
userApprovers, userApprovers,
groupApprovers, groupApprovers,
userBypassers, userBypassers,
@@ -281,7 +281,8 @@ const Form = ({
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
workspaceId: currentWorkspace?.id || "" workspaceId: currentWorkspace?.id || "",
environments: environments.map((env) => env.slug)
}); });
} else { } else {
await updateAccessApprovalPolicy({ await updateAccessApprovalPolicy({
@@ -297,7 +298,7 @@ const Form = ({
numberOfApprovals: el.approvals numberOfApprovals: el.approvals
})), })),
bypassers: bypassers.length > 0 ? bypassers : undefined, bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environments: environments.map((env) => env.slug),
projectSlug projectSlug
}); });
} }
@@ -479,14 +480,14 @@ const Form = ({
<SecretPathInput <SecretPathInput
{...field} {...field}
value={field.value || ""} value={field.value || ""}
environment={formEnvironment} environment={formEnvironments?.[0]?.slug || ""}
/> />
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="environment" name="environments"
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
label="Environment" label="Environment"
@@ -496,11 +497,11 @@ const Form = ({
className="flex-1" className="flex-1"
> >
<FilterableSelect <FilterableSelect
isDisabled={isEditMode}
value={value} value={value}
isMulti
onChange={onChange} onChange={onChange}
placeholder="Select environment..." placeholder="Select environment..."
options={environments} options={availableEnvironments}
getOptionValue={(option) => option.slug} getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
/> />
@@ -37,7 +37,7 @@ import { TWorkspaceUser } from "@app/hooks/api/users/types";
interface IPolicy { interface IPolicy {
id: string; id: string;
name: string; name: string;
environment: WorkspaceEnv; environments: WorkspaceEnv[];
projectId?: string; projectId?: string;
secretPath?: string; secretPath?: string;
approvals: number; approvals: number;
@@ -112,7 +112,7 @@ export const ApprovalPolicyRow = ({
onClick={() => setIsExpanded.toggle()} onClick={() => setIsExpanded.toggle()}
> >
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td> <Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td>
<Td>{policy.environment.name}</Td> <Td>{policy.environments.map((env) => env.name).join(", ")}</Td>
<Td>{policy.secretPath || "*"}</Td> <Td>{policy.secretPath || "*"}</Td>
<Td> <Td>
<Badge <Badge