mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 08:27:36 +00:00
feat: updated policy to have sequence order
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasStepColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "sequence");
|
||||||
|
const hasApprovalRequiredColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicyApprover,
|
||||||
|
"approvalsRequired"
|
||||||
|
);
|
||||||
|
if (!hasStepColumn || !hasApprovalRequiredColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (t) => {
|
||||||
|
if (!hasStepColumn) t.integer("sequence").defaultTo(0);
|
||||||
|
if (!hasApprovalRequiredColumn) t.integer("approvalsRequired").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasStepColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "sequence");
|
||||||
|
const hasApprovalRequiredColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicyApprover,
|
||||||
|
"approvalsRequired"
|
||||||
|
);
|
||||||
|
if (hasStepColumn || hasApprovalRequiredColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (t) => {
|
||||||
|
if (hasStepColumn) t.dropColumn("sequence");
|
||||||
|
if (hasApprovalRequiredColumn) t.dropColumn("approvalsRequired");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
approverUserId: z.string().uuid().nullable().optional(),
|
approverUserId: z.string().uuid().nullable().optional(),
|
||||||
approverGroupId: z.string().uuid().nullable().optional()
|
approverGroupId: z.string().uuid().nullable().optional(),
|
||||||
|
sequence: z.number().default(0).nullable().optional(),
|
||||||
|
approvalsRequired: z.number().default(1).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||||
|
|||||||
@@ -23,8 +23,17 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
approvers: z
|
approvers: z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
z.object({
|
||||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
|
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()
|
.array()
|
||||||
.max(100, "Cannot have more than 100 approvers")
|
.max(100, "Cannot have more than 100 approvers")
|
||||||
@@ -37,6 +46,14 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.array()
|
.array()
|
||||||
.max(100, "Cannot have more than 100 bypassers")
|
.max(100, "Cannot have more than 100 bypassers")
|
||||||
.optional(),
|
.optional(),
|
||||||
|
approvalsRequired: z
|
||||||
|
.record(
|
||||||
|
z.number().int(),
|
||||||
|
z.object({
|
||||||
|
numberOfApprovals: z.number().int()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||||
allowedSelfApprovals: z.boolean().default(true)
|
allowedSelfApprovals: z.boolean().default(true)
|
||||||
@@ -168,7 +185,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.optional(),
|
.optional(),
|
||||||
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),
|
||||||
|
approvalsRequired: z
|
||||||
|
.record(
|
||||||
|
z.number().int(),
|
||||||
|
z.object({
|
||||||
|
numberOfApprovals: z.number().int()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
TListAccessApprovalPoliciesDTO,
|
TListAccessApprovalPoliciesDTO,
|
||||||
TUpdateAccessApprovalPolicy
|
TUpdateAccessApprovalPolicy
|
||||||
} from "./access-approval-policy-types";
|
} from "./access-approval-policy-types";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
type TAccessApprovalPolicyServiceFactoryDep = {
|
type TAccessApprovalPolicyServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
@@ -76,24 +77,23 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
projectSlug,
|
projectSlug,
|
||||||
environment,
|
environment,
|
||||||
enforcementLevel,
|
enforcementLevel,
|
||||||
allowedSelfApprovals
|
allowedSelfApprovals,
|
||||||
|
approvalsRequired
|
||||||
}: TCreateAccessApprovalPolicy) => {
|
}: TCreateAccessApprovalPolicy) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
// If there is a group approver people might be added to the group later to meet the approvers quota
|
// If there is a group approver people might be added to the group later to meet the approvers quota
|
||||||
const groupApprovers = approvers
|
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
|
||||||
.filter((approver) => approver.type === ApproverType.Group)
|
|
||||||
.map((approver) => approver.id) as string[];
|
|
||||||
|
|
||||||
const userApprovers = approvers
|
const userApprovers = approvers.filter((approver) => approver.type === ApproverType.User && approver.id) as {
|
||||||
.filter((approver) => approver.type === ApproverType.User)
|
id: string;
|
||||||
.map((approver) => approver.id)
|
sequence?: number;
|
||||||
.filter(Boolean) as string[];
|
}[];
|
||||||
|
|
||||||
const userApproverNames = approvers
|
const userApproverNames = approvers.filter(
|
||||||
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
|
(approver) => approver.type === ApproverType.User && approver.username
|
||||||
.filter(Boolean) as string[];
|
) as { username: string; sequence?: number }[];
|
||||||
|
|
||||||
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
|
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
|
||||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
@@ -116,14 +116,13 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
|
|
||||||
let approverUserIds = userApprovers;
|
let approverUserIds = userApprovers;
|
||||||
if (userApproverNames.length) {
|
if (userApproverNames.length) {
|
||||||
const approverUsers = await userDAL.find({
|
const approverUsersInDB = await userDAL.find({
|
||||||
$in: {
|
$in: {
|
||||||
username: userApproverNames
|
username: userApproverNames.map((el) => el.username)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const approverUsersInDBGroupByUsername = groupBy(approverUsersInDB, (i) => i.username);
|
||||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
const invalidUsernames = userApproverNames.filter((el) => !approverUsersInDBGroupByUsername?.[el.username]?.[0]);
|
||||||
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
|
||||||
|
|
||||||
if (invalidUsernames.length) {
|
if (invalidUsernames.length) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -131,31 +130,13 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
|
approverUserIds = approverUserIds.concat(
|
||||||
}
|
userApproverNames.map((el) => ({
|
||||||
|
id: approverUsersInDBGroupByUsername[el.username]?.[0].username,
|
||||||
const usersPromises: Promise<
|
sequence: el.sequence
|
||||||
{
|
}))
|
||||||
id: string;
|
|
||||||
email: string | null | undefined;
|
|
||||||
username: string;
|
|
||||||
firstName: string | null | undefined;
|
|
||||||
lastName: string | null | undefined;
|
|
||||||
isPartOfGroup: boolean;
|
|
||||||
}[]
|
|
||||||
>[] = [];
|
|
||||||
const verifyAllApprovers = [...approverUserIds];
|
|
||||||
|
|
||||||
for (const groupId of groupApprovers) {
|
|
||||||
usersPromises.push(
|
|
||||||
groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }).then((group) => group.members)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
|
||||||
.flat()
|
|
||||||
.filter((user) => user.isPartOfGroup)
|
|
||||||
.map((user) => user.id);
|
|
||||||
verifyAllApprovers.push(...verifyGroupApprovers);
|
|
||||||
|
|
||||||
let groupBypassers: string[] = [];
|
let groupBypassers: string[] = [];
|
||||||
let bypasserUserIds: string[] = [];
|
let bypasserUserIds: string[] = [];
|
||||||
@@ -210,9 +191,11 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
|
|
||||||
if (approverUserIds.length) {
|
if (approverUserIds.length) {
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
approverUserIds.map((userId) => ({
|
approverUserIds.map((el) => ({
|
||||||
approverUserId: userId,
|
approverUserId: el.id,
|
||||||
policyId: doc.id
|
policyId: doc.id,
|
||||||
|
sequence: el.sequence,
|
||||||
|
approvalsRequired: el.sequence ? approvalsRequired?.[el.sequence]?.numberOfApprovals : approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -220,9 +203,11 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
|
|
||||||
if (groupApprovers) {
|
if (groupApprovers) {
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
groupApprovers.map((groupId) => ({
|
groupApprovers.map((el) => ({
|
||||||
approverGroupId: groupId,
|
approverGroupId: el.id,
|
||||||
policyId: doc.id
|
policyId: doc.id,
|
||||||
|
sequence: el.sequence,
|
||||||
|
approvalsRequired: el.sequence ? approvalsRequired?.[el.sequence]?.numberOfApprovals : approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -290,20 +275,19 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
approvals,
|
approvals,
|
||||||
enforcementLevel,
|
enforcementLevel,
|
||||||
allowedSelfApprovals
|
allowedSelfApprovals,
|
||||||
|
approvalsRequired
|
||||||
}: TUpdateAccessApprovalPolicy) => {
|
}: TUpdateAccessApprovalPolicy) => {
|
||||||
const groupApprovers = approvers
|
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
|
||||||
.filter((approver) => approver.type === ApproverType.Group)
|
|
||||||
.map((approver) => approver.id) as string[];
|
|
||||||
|
|
||||||
const userApprovers = approvers
|
const userApprovers = approvers.filter((approver) => approver.type === ApproverType.User && approver.id) as {
|
||||||
.filter((approver) => approver.type === ApproverType.User)
|
id: string;
|
||||||
.map((approver) => approver.id)
|
sequence?: number;
|
||||||
.filter(Boolean) as string[];
|
}[];
|
||||||
|
|
||||||
const userApproverNames = approvers
|
const userApproverNames = approvers.filter(
|
||||||
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
|
(approver) => approver.type === ApproverType.User && approver.username
|
||||||
.filter(Boolean) as string[];
|
) as { username: string; sequence?: number }[];
|
||||||
|
|
||||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||||
const currentApprovals = approvals || accessApprovalPolicy.approvals;
|
const currentApprovals = approvals || accessApprovalPolicy.approvals;
|
||||||
@@ -417,16 +401,18 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||||
|
|
||||||
if (userApprovers.length || userApproverNames.length) {
|
if (userApprovers.length || userApproverNames.length) {
|
||||||
let userApproverIds = userApprovers;
|
let approverUserIds = userApprovers;
|
||||||
if (userApproverNames.length) {
|
if (userApproverNames.length) {
|
||||||
const approverUsers = await userDAL.find({
|
const approverUsersInDB = await userDAL.find({
|
||||||
$in: {
|
$in: {
|
||||||
username: userApproverNames
|
username: userApproverNames.map((el) => el.username)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const approverUsersInDBGroupByUsername = groupBy(approverUsersInDB, (i) => i.username);
|
||||||
|
|
||||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
const invalidUsernames = userApproverNames.filter(
|
||||||
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
(el) => !approverUsersInDBGroupByUsername?.[el.username]?.[0]
|
||||||
|
);
|
||||||
|
|
||||||
if (invalidUsernames.length) {
|
if (invalidUsernames.length) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -434,13 +420,20 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
approverUserIds = approverUserIds.concat(
|
||||||
|
userApproverNames.map((el) => ({
|
||||||
|
id: approverUsersInDBGroupByUsername[el.username]?.[0].username,
|
||||||
|
sequence: el.sequence
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
userApproverIds.map((userId) => ({
|
approverUserIds.map((el) => ({
|
||||||
approverUserId: userId,
|
approverUserId: el.id,
|
||||||
policyId: doc.id
|
policyId: doc.id,
|
||||||
|
sequence: el.sequence,
|
||||||
|
approvalsRequired: el.sequence ? approvalsRequired?.[el.sequence]?.numberOfApprovals : approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -448,9 +441,11 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
|
|
||||||
if (groupApprovers) {
|
if (groupApprovers) {
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
groupApprovers.map((groupId) => ({
|
groupApprovers.map((el) => ({
|
||||||
approverGroupId: groupId,
|
approverGroupId: el.id,
|
||||||
policyId: doc.id
|
policyId: doc.id,
|
||||||
|
sequence: el.sequence,
|
||||||
|
approvalsRequired: el.sequence ? approvalsRequired?.[el.sequence]?.numberOfApprovals : approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ export type TCreateAccessApprovalPolicy = {
|
|||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
|
approvers: (
|
||||||
|
| { type: ApproverType.Group; id: string; sequence?: number }
|
||||||
|
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
|
||||||
|
)[];
|
||||||
bypassers?: (
|
bypassers?: (
|
||||||
| { type: BypasserType.Group; id: string }
|
| { type: BypasserType.Group; id: string }
|
||||||
| { type: BypasserType.User; id?: string; username?: string }
|
| { type: BypasserType.User; id?: string; username?: string }
|
||||||
@@ -36,12 +39,16 @@ export type TCreateAccessApprovalPolicy = {
|
|||||||
name: string;
|
name: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
allowedSelfApprovals: boolean;
|
allowedSelfApprovals: boolean;
|
||||||
|
approvalsRequired?: Record<number, { numberOfApprovals: number }>;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateAccessApprovalPolicy = {
|
export type TUpdateAccessApprovalPolicy = {
|
||||||
policyId: string;
|
policyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
|
approvers: (
|
||||||
|
| { type: ApproverType.Group; id: string; sequence?: number }
|
||||||
|
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
|
||||||
|
)[];
|
||||||
bypassers?: (
|
bypassers?: (
|
||||||
| { type: BypasserType.Group; id: string }
|
| { type: BypasserType.Group; id: string }
|
||||||
| { type: BypasserType.User; id?: string; username?: string }
|
| { type: BypasserType.User; id?: string; username?: string }
|
||||||
@@ -50,6 +57,7 @@ export type TUpdateAccessApprovalPolicy = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
allowedSelfApprovals: boolean;
|
allowedSelfApprovals: boolean;
|
||||||
|
approvalsRequired?: Record<number, { numberOfApprovals: number }>;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteAccessApprovalPolicy = {
|
export type TDeleteAccessApprovalPolicy = {
|
||||||
|
|||||||
Reference in New Issue
Block a user