Merge pull request #3777 from akhilmhdh/feat/seq-access-request
feat: Sequentail access approval request
@@ -0,0 +1,44 @@
|
|||||||
|
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(1);
|
||||||
|
if (!hasApprovalRequiredColumn) t.integer("approvalsRequired").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set rejected status for all access request that was rejected and still has status pending
|
||||||
|
const subquery = knex(TableName.AccessApprovalRequest)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.AccessApprovalRequestReviewer,
|
||||||
|
`${TableName.AccessApprovalRequestReviewer}.requestId`,
|
||||||
|
`${TableName.AccessApprovalRequest}.id`
|
||||||
|
)
|
||||||
|
.where(`${TableName.AccessApprovalRequest}.status` as "status", "pending")
|
||||||
|
.where(`${TableName.AccessApprovalRequestReviewer}.status` as "status", "rejected")
|
||||||
|
.select(`${TableName.AccessApprovalRequest}.id`);
|
||||||
|
|
||||||
|
await knex(TableName.AccessApprovalRequest).where("id", "in", subquery).update("status", "rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
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,12 +23,26 @@ 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")
|
||||||
.min(1, { message: "At least one approver should be provided" }),
|
.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
|
bypassers: z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||||
@@ -37,6 +51,13 @@ 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
|
||||||
|
.object({
|
||||||
|
numberOfApprovals: z.number().int(),
|
||||||
|
stepNumber: z.number().int()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.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)
|
||||||
@@ -78,7 +99,12 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
approvals: sapPubSchema
|
approvals: sapPubSchema
|
||||||
.extend({
|
.extend({
|
||||||
approvers: z
|
approvers: z
|
||||||
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
|
.object({
|
||||||
|
type: z.nativeEnum(ApproverType),
|
||||||
|
id: z.string().nullable().optional(),
|
||||||
|
sequence: z.number().nullable().optional(),
|
||||||
|
approvalsRequired: z.number().nullable().optional()
|
||||||
|
})
|
||||||
.array()
|
.array()
|
||||||
.nullable()
|
.nullable()
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -152,12 +178,26 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.transform((val) => (val === "" ? "/" : val)),
|
.transform((val) => (val === "" ? "/" : val)),
|
||||||
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()
|
||||||
.min(1, { message: "At least one approver should be provided" })
|
.min(1, { message: "At least one approver should be provided" })
|
||||||
.max(100, "Cannot have more than 100 approvers"),
|
.max(100, "Cannot have more than 100 approvers")
|
||||||
|
.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
|
bypassers: z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||||
@@ -168,7 +208,14 @@ 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
|
||||||
|
.object({
|
||||||
|
numberOfApprovals: z.number().int(),
|
||||||
|
stepNumber: z.number().int()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -235,7 +282,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.object({
|
.object({
|
||||||
type: z.nativeEnum(ApproverType),
|
type: z.nativeEnum(ApproverType),
|
||||||
id: z.string().nullable().optional(),
|
id: z.string().nullable().optional(),
|
||||||
name: z.string().nullable().optional()
|
name: z.string().nullable().optional(),
|
||||||
|
approvalsRequired: z.number().nullable().optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.nullable()
|
.nullable()
|
||||||
|
|||||||
@@ -112,7 +112,15 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: z.string().array(),
|
approvers: z
|
||||||
|
.object({
|
||||||
|
userId: z.string().nullable().optional(),
|
||||||
|
sequence: z.number().nullable().optional(),
|
||||||
|
approvalsRequired: z.number().nullable().optional(),
|
||||||
|
email: z.string().nullable().optional(),
|
||||||
|
username: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
bypassers: z.string().array(),
|
bypassers: z.string().array(),
|
||||||
secretPath: z.string().nullish(),
|
secretPath: z.string().nullish(),
|
||||||
envId: z.string(),
|
envId: z.string(),
|
||||||
|
|||||||
@@ -270,7 +270,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
schemas: z.array(z.string()),
|
schemas: z.array(z.string()),
|
||||||
id: z.string().trim(),
|
|
||||||
userName: z.string().trim(),
|
userName: z.string().trim(),
|
||||||
name: z
|
name: z
|
||||||
.object({
|
.object({
|
||||||
@@ -278,7 +277,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
givenName: z.string().trim().optional()
|
givenName: z.string().trim().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
displayName: z.string().trim(),
|
|
||||||
emails: z
|
emails: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
.select(tx.ref("username").withSchema("bypasserUsers").as("bypasserUsername"))
|
.select(tx.ref("username").withSchema("bypasserUsers").as("bypasserUsername"))
|
||||||
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(tx.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
|
||||||
|
.select(tx.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
.select(tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
|
.select(tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
|
||||||
.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"))
|
||||||
@@ -80,23 +82,31 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId: id }) => ({
|
mapper: ({ approverUserId: id, approverSequence, approvalsRequired }) => ({
|
||||||
id,
|
id,
|
||||||
type: "user"
|
type: "user",
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverGroupId",
|
key: "approverGroupId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverGroupId: id }) => ({
|
mapper: ({ approverGroupId: id, approverSequence, approvalsRequired }) => ({
|
||||||
id,
|
id,
|
||||||
type: "group"
|
type: "group",
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
if (!formattedDoc?.[0]) return;
|
||||||
|
|
||||||
return formattedDoc?.[0];
|
return {
|
||||||
|
...formattedDoc?.[0],
|
||||||
|
approvers: formattedDoc?.[0]?.approvers.sort((a, b) => (a.sequence || 1) - (b.sequence || 1))
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindById" });
|
throw new DatabaseError({ error, name: "FindById" });
|
||||||
}
|
}
|
||||||
@@ -129,18 +139,22 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId: id, approverUsername }) => ({
|
mapper: ({ approverUserId: id, approverUsername, approverSequence, approvalsRequired }) => ({
|
||||||
id,
|
id,
|
||||||
type: ApproverType.User,
|
type: ApproverType.User,
|
||||||
name: approverUsername
|
name: approverUsername,
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverGroupId",
|
key: "approverGroupId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverGroupId: id }) => ({
|
mapper: ({ approverGroupId: id, approverSequence, approvalsRequired }) => ({
|
||||||
id,
|
id,
|
||||||
type: ApproverType.Group
|
type: ApproverType.Group,
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -163,7 +177,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
return formattedDocs;
|
return formattedDocs.map((el) => ({
|
||||||
|
...el,
|
||||||
|
approvers: el?.approvers.sort((a, b) => (a.sequence || 1) - (b.sequence || 1))
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find" });
|
throw new DatabaseError({ error, name: "Find" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ActionProjectType } from "@app/db/schemas";
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
@@ -41,9 +42,9 @@ type TAccessApprovalPolicyServiceFactoryDep = {
|
|||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
groupDAL: TGroupDALFactory;
|
groupDAL: TGroupDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "find">;
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">;
|
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find" | "resetReviewByPolicyId">;
|
||||||
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">;
|
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,27 +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)
|
|
||||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -116,14 +113,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,32 +127,13 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
|
approverUserIds = approverUserIds.concat(
|
||||||
}
|
userApproverNames.map((el) => ({
|
||||||
|
id: approverUsersInDBGroupByUsername[el.username]?.[0].id,
|
||||||
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[] = [];
|
||||||
|
|
||||||
@@ -195,6 +172,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const approvalsRequiredGroupByStepNumber = groupBy(approvalsRequired || [], (i) => i.stepNumber);
|
||||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
const doc = await accessApprovalPolicyDAL.create(
|
const doc = await accessApprovalPolicyDAL.create(
|
||||||
{
|
{
|
||||||
@@ -210,9 +188,13 @@ 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
|
||||||
|
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
|
||||||
|
: approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -220,9 +202,13 @@ 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
|
||||||
|
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
|
||||||
|
: approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -290,22 +276,22 @@ 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.filter(
|
||||||
const userApproverNames = approvers
|
(approver) => approver.type === ApproverType.User && approver.username
|
||||||
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
|
) as { username: string; sequence?: number }[];
|
||||||
.filter(Boolean) as string[];
|
|
||||||
|
|
||||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||||
|
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Approval policy not found" });
|
||||||
|
|
||||||
const currentApprovals = approvals || accessApprovalPolicy.approvals;
|
const currentApprovals = approvals || accessApprovalPolicy.approvals;
|
||||||
if (
|
if (
|
||||||
groupApprovers?.length === 0 &&
|
groupApprovers?.length === 0 &&
|
||||||
@@ -401,6 +387,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const approvalsRequiredGroupByStepNumber = groupBy(approvalsRequired || [], (i) => i.stepNumber);
|
||||||
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
const doc = await accessApprovalPolicyDAL.updateById(
|
const doc = await accessApprovalPolicyDAL.updateById(
|
||||||
accessApprovalPolicy.id,
|
accessApprovalPolicy.id,
|
||||||
@@ -417,16 +404,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 +423,21 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
approverUserIds = approverUserIds.concat(
|
||||||
|
userApproverNames.map((el) => ({
|
||||||
|
id: approverUsersInDBGroupByUsername[el.username]?.[0].id,
|
||||||
|
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
|
||||||
|
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
|
||||||
|
: approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -448,9 +445,13 @@ 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
|
||||||
|
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
|
||||||
|
: approvals
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -478,6 +479,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await accessApprovalRequestDAL.resetReviewByPolicyId(doc.id, tx);
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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?: { numberOfApprovals: number; stepNumber: 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?: { numberOfApprovals: number; stepNumber: number }[];
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteAccessApprovalPolicy = {
|
export type TDeleteAccessApprovalPolicy = {
|
||||||
|
|||||||
@@ -39,12 +39,16 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.AccessApprovalRequest}.id`,
|
`${TableName.AccessApprovalRequest}.id`,
|
||||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||||
)
|
)
|
||||||
|
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.AccessApprovalPolicyApprover,
|
TableName.AccessApprovalPolicyApprover,
|
||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||||
|
"accessApprovalPolicyApproverUser.id"
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.UserGroupMembership,
|
TableName.UserGroupMembership,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||||
@@ -82,13 +86,18 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
||||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
|
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
|
||||||
)
|
)
|
||||||
|
|
||||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
|
||||||
|
.select(db.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
|
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
|
||||||
|
|
||||||
.select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
|
.select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
|
||||||
.select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"))
|
.select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"))
|
||||||
|
.select(
|
||||||
|
db.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
|
db.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
|
||||||
|
db.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
|
||||||
|
db.ref("username").withSchema(TableName.Users).as("approverGroupUsername")
|
||||||
|
)
|
||||||
.select(
|
.select(
|
||||||
db.ref("projectId").withSchema(TableName.Environment),
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
@@ -164,8 +173,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
permissions: doc.privilegePermissions
|
permissions: doc.privilegePermissions
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
isApproved: doc.status === ApprovalStatus.APPROVED
|
||||||
isApproved: !!doc.policyDeletedAt || !!doc.privilegeId || doc.status !== ApprovalStatus.PENDING
|
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
@@ -173,11 +181,33 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
||||||
},
|
},
|
||||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
|
{
|
||||||
|
key: "approverUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({
|
||||||
|
userId: approverUserId,
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired,
|
||||||
|
email: approverEmail,
|
||||||
|
username: approverUsername
|
||||||
|
})
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "approverGroupUserId",
|
key: "approverGroupUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverGroupUserId }) => approverGroupUserId
|
mapper: ({
|
||||||
|
approverGroupUserId,
|
||||||
|
approverSequence,
|
||||||
|
approvalsRequired,
|
||||||
|
approverGroupEmail,
|
||||||
|
approverGroupUsername
|
||||||
|
}) => ({
|
||||||
|
userId: approverGroupUserId,
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired,
|
||||||
|
email: approverGroupEmail,
|
||||||
|
username: approverGroupUsername
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
||||||
{
|
{
|
||||||
@@ -192,7 +222,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return formattedDocs.map((doc) => ({
|
return formattedDocs.map((doc) => ({
|
||||||
...doc,
|
...doc,
|
||||||
policy: { ...doc.policy, approvers: doc.approvers, bypassers: doc.bypassers }
|
policy: {
|
||||||
|
...doc.policy,
|
||||||
|
approvers: doc.approvers.filter((el) => el.userId).sort((a, b) => (a.sequence || 0) - (b.sequence || 0)),
|
||||||
|
bypassers: doc.bypassers
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
|
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
|
||||||
@@ -272,6 +306,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
||||||
|
tx.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"),
|
||||||
|
tx.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover),
|
||||||
tx.ref("userId").withSchema(TableName.UserGroupMembership),
|
tx.ref("userId").withSchema(TableName.UserGroupMembership),
|
||||||
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||||
@@ -367,13 +403,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approverEmail: email,
|
approverEmail: email,
|
||||||
approverUsername: username,
|
approverUsername: username,
|
||||||
approverLastName: lastName,
|
approverLastName: lastName,
|
||||||
approverFirstName: firstName
|
approverFirstName: firstName,
|
||||||
|
approverSequence,
|
||||||
|
approvalsRequired
|
||||||
}) => ({
|
}) => ({
|
||||||
userId: approverUserId,
|
userId: approverUserId,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
username
|
username,
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -384,13 +424,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approverGroupEmail: email,
|
approverGroupEmail: email,
|
||||||
approverGroupUsername: username,
|
approverGroupUsername: username,
|
||||||
approverGroupLastName: lastName,
|
approverGroupLastName: lastName,
|
||||||
approverFirstName: firstName
|
approverFirstName: firstName,
|
||||||
|
approverSequence,
|
||||||
|
approvalsRequired
|
||||||
}) => ({
|
}) => ({
|
||||||
userId,
|
userId,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
username
|
username,
|
||||||
|
sequence: approverSequence,
|
||||||
|
approvalsRequired
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -434,7 +478,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
...formattedDoc[0],
|
...formattedDoc[0],
|
||||||
policy: {
|
policy: {
|
||||||
...formattedDoc[0].policy,
|
...formattedDoc[0].policy,
|
||||||
approvers: formattedDoc[0].approvers,
|
approvers: formattedDoc[0].approvers
|
||||||
|
.filter((el) => el.userId)
|
||||||
|
.sort((a, b) => (a.sequence || 0) - (b.sequence || 0)),
|
||||||
bypassers: formattedDoc[0].bypassers
|
bypassers: formattedDoc[0].bypassers
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -495,7 +541,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
req.status === ApprovalStatus.PENDING
|
req.status === ApprovalStatus.PENDING
|
||||||
);
|
);
|
||||||
|
|
||||||
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required
|
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
|
||||||
const finalizedApprovals = formattedRequests.filter(
|
const finalizedApprovals = formattedRequests.filter(
|
||||||
(req) =>
|
(req) =>
|
||||||
req.privilegeId ||
|
req.privilegeId ||
|
||||||
@@ -509,5 +555,27 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount };
|
const resetReviewByPolicyId = async (policyId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
await (tx || db)(TableName.AccessApprovalRequestReviewer)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.AccessApprovalRequest,
|
||||||
|
`${TableName.AccessApprovalRequest}.id`,
|
||||||
|
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||||
|
)
|
||||||
|
.where(`${TableName.AccessApprovalRequest}.status` as "status", ApprovalStatus.PENDING)
|
||||||
|
.where(`${TableName.AccessApprovalRequest}.policyId` as "policyId", policyId)
|
||||||
|
.del();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "ResetReviewByPolicyId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...accessApprovalRequestOrm,
|
||||||
|
findById,
|
||||||
|
findRequestsWithPrivilegeByPolicyIds,
|
||||||
|
getCount,
|
||||||
|
resetReviewByPolicyId
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import msFn from "ms";
|
|||||||
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
@@ -358,7 +359,6 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
|
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
|
||||||
|
|
||||||
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
|
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
|
||||||
|
|
||||||
// If user is (not an approver OR cant self approve) AND can't bypass policy
|
// If user is (not an approver OR cant self approve) AND can't bypass policy
|
||||||
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
|
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -380,8 +380,44 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
if (accessApprovalRequest.status !== ApprovalStatus.PENDING) {
|
||||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
throw new BadRequestError({ message: "The request has been closed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewsGroupById = groupBy(
|
||||||
|
existingReviews.filter((review) => review.status === ApprovalStatus.APPROVED),
|
||||||
|
(i) => i.reviewerUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
const approvedSequences = policy.approvers.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
const hasApproved = reviewsGroupById?.[curr.userId as string]?.[0];
|
||||||
|
if (acc?.[acc.length - 1]?.step === curr.sequence) {
|
||||||
|
if (hasApproved) {
|
||||||
|
acc[acc.length - 1].approvals += 1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
step: curr.sequence || 1,
|
||||||
|
approvals: hasApproved ? 1 : 0,
|
||||||
|
requiredApprovals: curr.approvalsRequired || 1
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as { step: number; approvals: number; requiredApprovals: number }[]
|
||||||
|
);
|
||||||
|
const presentSequence = approvedSequences.find((el) => el.approvals < el.requiredApprovals) || {
|
||||||
|
step: 1,
|
||||||
|
approvals: 0,
|
||||||
|
requiredApprovals: 1
|
||||||
|
};
|
||||||
|
if (presentSequence) {
|
||||||
|
const isApproverOfTheSequence = policy.approvers.find(
|
||||||
|
(el) => el.sequence === presentSequence.step && el.userId === actorId
|
||||||
|
);
|
||||||
|
if (!isApproverOfTheSequence) throw new BadRequestError({ message: "You are not reviewer in this step" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
|
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
|
||||||
@@ -426,11 +462,14 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherReviews = existingReviews.filter((er) => er.reviewerUserId !== actorId);
|
if (status === ApprovalStatus.REJECTED) {
|
||||||
const allUniqueReviews = [...otherReviews, reviewForThisActorProcessing];
|
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { status: ApprovalStatus.REJECTED }, tx);
|
||||||
|
return reviewForThisActorProcessing;
|
||||||
|
}
|
||||||
|
|
||||||
const approvedReviews = allUniqueReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
|
const meetsStandardApprovalThreshold =
|
||||||
const meetsStandardApprovalThreshold = approvedReviews.length >= policy.approvals;
|
(presentSequence?.approvals || 0) + 1 >= presentSequence.requiredApprovals &&
|
||||||
|
approvedSequences.at(-1)?.step === presentSequence?.step;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
reviewForThisActorProcessing.status === ApprovalStatus.APPROVED &&
|
reviewForThisActorProcessing.status === ApprovalStatus.APPROVED &&
|
||||||
|
|||||||
@@ -106,9 +106,11 @@ export const userServiceFactory = ({
|
|||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
|
||||||
|
|
||||||
await userDAL.updateById(user.id, {
|
await userDAL.updateById(user.id, {
|
||||||
isEmailVerified: true,
|
isEmailVerified: true,
|
||||||
username: usersByusername.length === 1 && user.email ? user.email.toLowerCase() : undefined
|
username: userEmails?.length === 1 && userEmails?.[0]?.id === user.id ? user.email.toLowerCase() : undefined
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,21 +6,29 @@ description: "Learn how to request access to sensitive resources in Infisical."
|
|||||||
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
|
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
|
||||||
|
|
||||||
This functionality works in the following way:
|
This functionality works in the following way:
|
||||||
|
|
||||||
1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
|
1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
|
||||||

|

|
||||||

|
|
||||||
|
<Note>
|
||||||
|
A step policy enables a sequential approval workflow in which approvals
|
||||||
|
must follow the designated chain.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
2. When a developer requests access to one of such sensitive resources, the request is visible in the dashboard, and the corresponding eligible approvers get an email notification about it.
|
2. When a developer requests access to one of such sensitive resources, the request is visible in the dashboard, and the corresponding eligible approvers get an email notification about it.
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
4. An eligible approver can approve or reject the access request.
|
3. An eligible approver can approve or reject the access request.
|
||||||
{/*  */}
|
{/*  */}
|
||||||

|

|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
If the access request matches with a policy that allows break-glass approval bypasses, the requester may bypass the policy and get access to the resource without full approval.
|
If the access request matches with a policy that allows break-glass approval
|
||||||
|
bypasses, the requester may bypass the policy and get access to the resource
|
||||||
|
without full approval.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
5. As soon as the request is approved, developer is able to access the sought resources.
|
5. As soon as the request is approved, developer is able to access the sought resources.
|
||||||

|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 468 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 536 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 479 KiB |
@@ -1150,9 +1150,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.4.1",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
|
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3640,9 +3640,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@stylistic/eslint-plugin": {
|
"node_modules/@stylistic/eslint-plugin": {
|
||||||
"version": "2.12.1",
|
"version": "2.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz",
|
||||||
"integrity": "sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==",
|
"integrity": "sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3904,9 +3904,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
||||||
"integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==",
|
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3916,7 +3916,7 @@
|
|||||||
"postcss-selector-parser": "6.0.10"
|
"postcss-selector-parser": "6.0.10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
|
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||||
@@ -3934,13 +3934,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/eslint-plugin-router": {
|
"node_modules/@tanstack/eslint-plugin-router": {
|
||||||
"version": "1.87.6",
|
"version": "1.120.17",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-router/-/eslint-plugin-router-1.87.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-router/-/eslint-plugin-router-1.120.17.tgz",
|
||||||
"integrity": "sha512-HoJYMI8Jcsdk4Q357bSFykDIpmU+PCAhm9IQpbcPF+wuRITHBBivLy6poaM9X184ng6FDHUOTbt6L8ZF6dYfVw==",
|
"integrity": "sha512-dYnfQ2on0i9JOonvuP6y6AH47Yhj3zQHxa5rCuLOrpKRUR3Mjpp+4AB70ltY1DKBVWtp1moyruJqueD8kzHD5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/utils": "^8.18.0"
|
"@typescript-eslint/utils": "^8.23.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4474,21 +4474,21 @@
|
|||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz",
|
||||||
"integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==",
|
"integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.18.0",
|
"@typescript-eslint/scope-manager": "8.34.0",
|
||||||
"@typescript-eslint/type-utils": "8.18.0",
|
"@typescript-eslint/type-utils": "8.34.0",
|
||||||
"@typescript-eslint/utils": "8.18.0",
|
"@typescript-eslint/utils": "8.34.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.18.0",
|
"@typescript-eslint/visitor-keys": "8.34.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.3.1",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -4498,22 +4498,32 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
|
"@typescript-eslint/parser": "^8.34.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.8.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz",
|
||||||
"integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==",
|
"integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MITClause",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.18.0",
|
"@typescript-eslint/scope-manager": "8.34.0",
|
||||||
"@typescript-eslint/types": "8.18.0",
|
"@typescript-eslint/types": "8.34.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.18.0",
|
"@typescript-eslint/typescript-estree": "8.34.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.18.0",
|
"@typescript-eslint/visitor-keys": "8.34.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4525,18 +4535,40 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.8.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz",
|
||||||
"integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==",
|
"integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.18.0",
|
"@typescript-eslint/tsconfig-utils": "^8.34.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.18.0"
|
"@typescript-eslint/types": "^8.34.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
|
"version": "8.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz",
|
||||||
|
"integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.34.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -4546,17 +4578,34 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
|
"version": "8.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz",
|
||||||
|
"integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz",
|
||||||
"integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==",
|
"integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.18.0",
|
"@typescript-eslint/typescript-estree": "8.34.0",
|
||||||
"@typescript-eslint/utils": "8.18.0",
|
"@typescript-eslint/utils": "8.34.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -4567,13 +4616,13 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.8.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz",
|
||||||
"integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==",
|
"integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4585,20 +4634,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz",
|
||||||
"integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==",
|
"integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.18.0",
|
"@typescript-eslint/project-service": "8.34.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.18.0",
|
"@typescript-eslint/tsconfig-utils": "8.34.0",
|
||||||
|
"@typescript-eslint/types": "8.34.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.34.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^9.0.4",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -4608,13 +4659,13 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=4.8.4 <5.8.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4638,16 +4689,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz",
|
||||||
"integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==",
|
"integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.18.0",
|
"@typescript-eslint/scope-manager": "8.34.0",
|
||||||
"@typescript-eslint/types": "8.18.0",
|
"@typescript-eslint/types": "8.34.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.18.0"
|
"@typescript-eslint/typescript-estree": "8.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -4658,17 +4709,17 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.8.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz",
|
||||||
"integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==",
|
"integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.18.0",
|
"@typescript-eslint/types": "8.34.0",
|
||||||
"eslint-visitor-keys": "^4.2.0"
|
"eslint-visitor-keys": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -12624,9 +12675,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sucrase/node_modules/brace-expansion": {
|
"node_modules/sucrase/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -12733,9 +12784,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.16",
|
"version": "3.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
"integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==",
|
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -12883,16 +12934,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "1.4.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
|
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18.12"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=4.2.0"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-interface-checker": {
|
"node_modules/ts-interface-checker": {
|
||||||
@@ -13550,15 +13601,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.18.0",
|
"version": "8.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz",
|
||||||
"integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==",
|
"integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.18.0",
|
"@typescript-eslint/eslint-plugin": "8.34.0",
|
||||||
"@typescript-eslint/parser": "8.18.0",
|
"@typescript-eslint/parser": "8.34.0",
|
||||||
"@typescript-eslint/utils": "8.18.0"
|
"@typescript-eslint/utils": "8.34.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -13569,7 +13620,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.8.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
@@ -13908,9 +13959,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.18",
|
"version": "5.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
|
||||||
"integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
|
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ export const FilterableSelect = <T,>({
|
|||||||
control: (base) => ({
|
control: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
transition: "none"
|
transition: "none"
|
||||||
|
}),
|
||||||
|
menuPortal: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
zIndex: 9999
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
tabSelectsValue={tabSelectsValue}
|
tabSelectsValue={tabSelectsValue}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export const useCreateAccessApprovalPolicy = () => {
|
|||||||
name,
|
name,
|
||||||
secretPath,
|
secretPath,
|
||||||
enforcementLevel,
|
enforcementLevel,
|
||||||
allowedSelfApprovals
|
allowedSelfApprovals,
|
||||||
|
approvalsRequired
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
||||||
environment,
|
environment,
|
||||||
@@ -36,7 +37,8 @@ export const useCreateAccessApprovalPolicy = () => {
|
|||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel,
|
enforcementLevel,
|
||||||
allowedSelfApprovals
|
allowedSelfApprovals,
|
||||||
|
approvalsRequired
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -60,7 +62,8 @@ export const useUpdateAccessApprovalPolicy = () => {
|
|||||||
name,
|
name,
|
||||||
secretPath,
|
secretPath,
|
||||||
enforcementLevel,
|
enforcementLevel,
|
||||||
allowedSelfApprovals
|
allowedSelfApprovals,
|
||||||
|
approvalsRequired
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
||||||
approvals,
|
approvals,
|
||||||
@@ -69,7 +72,8 @@ export const useUpdateAccessApprovalPolicy = () => {
|
|||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel,
|
enforcementLevel,
|
||||||
allowedSelfApprovals
|
allowedSelfApprovals,
|
||||||
|
approvalsRequired
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EnforcementLevel, PolicyType } from "../policies/enums";
|
import { EnforcementLevel, PolicyType } from "../policies/enums";
|
||||||
import { TProjectPermission } from "../roles/types";
|
import { TProjectPermission } from "../roles/types";
|
||||||
|
import { ApprovalStatus } from "../secretApprovalRequest/types";
|
||||||
import { WorkspaceEnv } from "../workspace/types";
|
import { WorkspaceEnv } from "../workspace/types";
|
||||||
|
|
||||||
export type TAccessApprovalPolicy = {
|
export type TAccessApprovalPolicy = {
|
||||||
@@ -33,6 +34,8 @@ export enum BypasserType {
|
|||||||
export type Approver = {
|
export type Approver = {
|
||||||
id: string;
|
id: string;
|
||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
|
sequence?: number;
|
||||||
|
approvals?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Bypasser = {
|
export type Bypasser = {
|
||||||
@@ -73,12 +76,18 @@ export type TAccessApprovalRequest = {
|
|||||||
permissions: TProjectPermission[];
|
permissions: TProjectPermission[];
|
||||||
isApproved: boolean;
|
isApproved: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
status: ApprovalStatus;
|
||||||
policy: {
|
policy: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
approvals: number;
|
approvals: number;
|
||||||
approvers: string[];
|
approvers: {
|
||||||
|
userId: string;
|
||||||
|
sequence?: number;
|
||||||
|
approvalsRequired?: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
}[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
envId: string;
|
envId: string;
|
||||||
@@ -88,7 +97,7 @@ export type TAccessApprovalRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
reviewers: {
|
reviewers: {
|
||||||
member: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
@@ -163,6 +172,7 @@ export type TCreateAccessPolicyDTO = {
|
|||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
allowedSelfApprovals: boolean;
|
allowedSelfApprovals: boolean;
|
||||||
|
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateAccessPolicyDTO = {
|
export type TUpdateAccessPolicyDTO = {
|
||||||
@@ -177,6 +187,7 @@ export type TUpdateAccessPolicyDTO = {
|
|||||||
allowedSelfApprovals: boolean;
|
allowedSelfApprovals: boolean;
|
||||||
// for invalidating list
|
// for invalidating list
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteSecretPolicyDTO = {
|
export type TDeleteSecretPolicyDTO = {
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export const useGetSecretApprovalRequestCount = ({
|
|||||||
}) =>
|
}) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: secretApprovalRequestKeys.count({ workspaceId }),
|
queryKey: secretApprovalRequestKeys.count({ workspaceId }),
|
||||||
refetchInterval: 5000,
|
refetchInterval: 15000,
|
||||||
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
|
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
|
||||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
|
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export const AccessApprovalRequest = ({
|
|||||||
isRequestedByCurrentUser: boolean;
|
isRequestedByCurrentUser: boolean;
|
||||||
isSelfApproveAllowed: boolean;
|
isSelfApproveAllowed: boolean;
|
||||||
isApprover: boolean;
|
isApprover: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
})
|
})
|
||||||
| null
|
| null
|
||||||
>(null);
|
>(null);
|
||||||
@@ -147,16 +148,17 @@ export const AccessApprovalRequest = ({
|
|||||||
const generateRequestDetails = useCallback(
|
const generateRequestDetails = useCallback(
|
||||||
(request: TAccessApprovalRequest) => {
|
(request: TAccessApprovalRequest) => {
|
||||||
const isReviewedByUser =
|
const isReviewedByUser =
|
||||||
request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
|
request.reviewers.findIndex(({ userId }) => userId === user.id) !== -1;
|
||||||
const isRejectedByAnyone = request.reviewers.some(
|
const isRejectedByAnyone = request.reviewers.some(
|
||||||
({ status }) => status === ApprovalStatus.REJECTED
|
({ status }) => status === ApprovalStatus.REJECTED
|
||||||
);
|
);
|
||||||
const isApprover = request.policy.approvers.indexOf(user.id || "") !== -1;
|
const isApprover =
|
||||||
|
request.policy.approvers.findIndex((el) => el.userId === user.id || "") !== -1;
|
||||||
const isAccepted = request.isApproved;
|
const isAccepted = request.isApproved;
|
||||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||||
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
|
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
|
||||||
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
|
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
|
||||||
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
|
const userReviewStatus = request.reviewers.find(({ userId }) => userId === user.id)?.status;
|
||||||
const canBypass =
|
const canBypass =
|
||||||
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
|
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
|
||||||
|
|
||||||
@@ -205,21 +207,6 @@ export const AccessApprovalRequest = ({
|
|||||||
const handleSelectRequest = useCallback(
|
const handleSelectRequest = useCallback(
|
||||||
(request: TAccessApprovalRequest) => {
|
(request: TAccessApprovalRequest) => {
|
||||||
const details = generateRequestDetails(request);
|
const details = generateRequestDetails(request);
|
||||||
|
|
||||||
// Whether the request has already been approved / rejected / reviewed
|
|
||||||
const isInactive =
|
|
||||||
details.isAccepted || details.isReviewedByUser || details.isRejectedByAnyone;
|
|
||||||
|
|
||||||
// Whether the current user can bypass policy
|
|
||||||
const canBypass =
|
|
||||||
details.isSoftEnforcement && details.isRequestedByCurrentUser && details.canBypass;
|
|
||||||
|
|
||||||
// Whether the current user can approve
|
|
||||||
const canApprove =
|
|
||||||
details.isApprover && (!details.isRequestedByCurrentUser || details.isSelfApproveAllowed);
|
|
||||||
|
|
||||||
if (isInactive || (!canApprove && !canBypass)) return;
|
|
||||||
|
|
||||||
if (membersGroupById?.[request.requestedByUserId].user || details.isRequestedByCurrentUser) {
|
if (membersGroupById?.[request.requestedByUserId].user || details.isRequestedByCurrentUser) {
|
||||||
setSelectedRequest({
|
setSelectedRequest({
|
||||||
...request,
|
...request,
|
||||||
@@ -381,9 +368,6 @@ export const AccessApprovalRequest = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-disabled={
|
|
||||||
details.isReviewedByUser || details.isRejectedByAnyone || details.isAccepted
|
|
||||||
}
|
|
||||||
key={request.id}
|
key={request.id}
|
||||||
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -414,11 +398,9 @@ export const AccessApprovalRequest = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{details.isApprover && (
|
<Badge variant={details.displayData.type}>
|
||||||
<Badge variant={details.displayData.type}>
|
{details.displayData.label}
|
||||||
{details.displayData.label}
|
</Badge>
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -450,9 +432,11 @@ export const AccessApprovalRequest = ({
|
|||||||
{!!selectedRequest && (
|
{!!selectedRequest && (
|
||||||
<ReviewAccessRequestModal
|
<ReviewAccessRequestModal
|
||||||
selectedEnvSlug={envFilter}
|
selectedEnvSlug={envFilter}
|
||||||
|
policies={policies || []}
|
||||||
selectedRequester={requestedByFilter}
|
selectedRequester={requestedByFilter}
|
||||||
projectSlug={projectSlug}
|
projectSlug={projectSlug}
|
||||||
request={selectedRequest}
|
request={selectedRequest}
|
||||||
|
members={members || []}
|
||||||
isOpen={popUp.reviewRequest.isOpen}
|
isOpen={popUp.reviewRequest.isOpen}
|
||||||
onOpenChange={() => {
|
onOpenChange={() => {
|
||||||
handlePopUpClose("reviewRequest");
|
handlePopUpClose("reviewRequest");
|
||||||
|
|||||||
@@ -1,16 +1,48 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheckCircle,
|
||||||
|
faCircle,
|
||||||
|
faTriangleExclamation,
|
||||||
|
faUsers,
|
||||||
|
faXmarkCircle
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, Checkbox, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
import { ProjectPermissionActions } from "@app/context";
|
import { ProjectPermissionActions, useUser, useWorkspace } from "@app/context";
|
||||||
import { useReviewAccessRequest } from "@app/hooks/api";
|
import { useListWorkspaceGroups, useReviewAccessRequest } from "@app/hooks/api";
|
||||||
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
|
import {
|
||||||
|
Approver,
|
||||||
|
ApproverType,
|
||||||
|
TAccessApprovalPolicy,
|
||||||
|
TAccessApprovalRequest
|
||||||
|
} from "@app/hooks/api/accessApproval/types";
|
||||||
import { EnforcementLevel } from "@app/hooks/api/policies/enums";
|
import { EnforcementLevel } from "@app/hooks/api/policies/enums";
|
||||||
|
import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
import { groupBy } from "@app/lib/fn/array";
|
||||||
|
|
||||||
|
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
||||||
|
if (status === ApprovalStatus.APPROVED)
|
||||||
|
return <FontAwesomeIcon icon={faCheckCircle} size="xs" style={{ color: "#15803d" }} />;
|
||||||
|
if (status === ApprovalStatus.REJECTED)
|
||||||
|
return <FontAwesomeIcon icon={faXmarkCircle} size="xs" style={{ color: "#b91c1c" }} />;
|
||||||
|
return <FontAwesomeIcon icon={faCircle} size="xs" style={{ color: "#c2410c" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
export const ReviewAccessRequestModal = ({
|
export const ReviewAccessRequestModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -19,7 +51,9 @@ export const ReviewAccessRequestModal = ({
|
|||||||
projectSlug,
|
projectSlug,
|
||||||
selectedRequester,
|
selectedRequester,
|
||||||
selectedEnvSlug,
|
selectedEnvSlug,
|
||||||
canBypass
|
canBypass,
|
||||||
|
policies = [],
|
||||||
|
members = []
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
@@ -33,10 +67,15 @@ export const ReviewAccessRequestModal = ({
|
|||||||
selectedRequester: string | undefined;
|
selectedRequester: string | undefined;
|
||||||
selectedEnvSlug: string | undefined;
|
selectedEnvSlug: string | undefined;
|
||||||
canBypass: boolean;
|
canBypass: boolean;
|
||||||
|
policies: TAccessApprovalPolicy[];
|
||||||
|
members: TWorkspaceUser[];
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
||||||
const [bypassApproval, setBypassApproval] = useState(false);
|
const [bypassApproval, setBypassApproval] = useState(false);
|
||||||
const [bypassReason, setBypassReason] = useState("");
|
const [bypassReason, setBypassReason] = useState("");
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || "");
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||||
|
|
||||||
@@ -134,6 +173,79 @@ export const ReviewAccessRequestModal = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const approverSequence = useMemo(() => {
|
||||||
|
const policy = policies.find((el) => el.id === request.policy.id);
|
||||||
|
const reviewesGroupById = groupBy(request.reviewers, (i) => i.userId);
|
||||||
|
const membersGroupById = groupBy(members, (i) => i.user.id);
|
||||||
|
const projectGroupsGroupById = groupBy(groupMemberships, (i) => i.group.id);
|
||||||
|
const approversBySequence = policy?.approvers?.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
||||||
|
acc[acc.length - 1][curr.type]?.push(curr);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const approvals = curr.approvals || policy.approvals;
|
||||||
|
const sequence = curr.sequence || 1;
|
||||||
|
|
||||||
|
acc.push(
|
||||||
|
curr.type === ApproverType.User
|
||||||
|
? { user: [curr], group: [], sequence, approvals }
|
||||||
|
: { group: [curr], user: [], sequence, approvals }
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as {
|
||||||
|
user: Approver[];
|
||||||
|
group: Approver[];
|
||||||
|
sequence?: number;
|
||||||
|
approvals?: number;
|
||||||
|
}[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const approvers = approversBySequence?.map((approverChain) => {
|
||||||
|
const reviewers = request.policy.approvers
|
||||||
|
.filter((el) => (el.sequence || 1) === approverChain.sequence)
|
||||||
|
.map((el) => ({ ...el, status: reviewesGroupById?.[el.userId]?.[0]?.status }));
|
||||||
|
const hasApproved =
|
||||||
|
reviewers.filter((el) => el.status === "approved").length >=
|
||||||
|
(approverChain?.approvals || 1);
|
||||||
|
|
||||||
|
const hasRejected = reviewers.filter((el) => el.status === ApprovalStatus.REJECTED).length;
|
||||||
|
return { ...approverChain, reviewers, hasApproved, hasRejected };
|
||||||
|
});
|
||||||
|
const currentSequenceApprover = approvers?.find((el) => !el.hasApproved);
|
||||||
|
const currentSequence = currentSequenceApprover?.sequence || 1;
|
||||||
|
const isMyReviewInThisSequence = currentSequenceApprover?.reviewers.find(
|
||||||
|
(i) => i.userId === user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
approvers,
|
||||||
|
membersGroupById,
|
||||||
|
projectGroupsGroupById,
|
||||||
|
currentSequence,
|
||||||
|
isMyReviewInThisSequence
|
||||||
|
};
|
||||||
|
}, [request, policies]);
|
||||||
|
|
||||||
|
const hasRejected = request.status === ApprovalStatus.REJECTED;
|
||||||
|
const hasApproved = request.status === ApprovalStatus.APPROVED;
|
||||||
|
const isReviewedByMe = request.reviewers.find((i) => i.userId === user.id);
|
||||||
|
|
||||||
|
const shouldBlockRequestActions =
|
||||||
|
hasRejected ||
|
||||||
|
hasApproved ||
|
||||||
|
isReviewedByMe ||
|
||||||
|
(!approverSequence?.isMyReviewInThisSequence && !canBypass);
|
||||||
|
|
||||||
|
const renderCompletedMessages = () => {
|
||||||
|
if (hasRejected) return "This request has been rejected.";
|
||||||
|
if (hasApproved) return "This request has been approved.";
|
||||||
|
if (isReviewedByMe) return "You have reviewed this request.";
|
||||||
|
return "You are not the reviewer in this step.";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
@@ -141,106 +253,236 @@ export const ReviewAccessRequestModal = ({
|
|||||||
title="Review Request"
|
title="Review Request"
|
||||||
subTitle="Review the request and approve or deny access."
|
subTitle="Review the request and approve or deny access."
|
||||||
>
|
>
|
||||||
<div className="text-sm">
|
<div className="mb-4 rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5 text-sm">
|
||||||
<span>
|
{request.user &&
|
||||||
{request.user &&
|
(request.user.firstName || request.user.lastName) &&
|
||||||
(request.user.firstName || request.user.lastName) &&
|
request.user.email ? (
|
||||||
request.user.email ? (
|
<span className="inline font-bold">
|
||||||
<span className="font-bold">
|
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
||||||
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
</span>
|
||||||
</span>
|
) : (
|
||||||
) : (
|
<span>A user</span>
|
||||||
<span>A user</span>
|
)}{" "}
|
||||||
)}{" "}
|
is requesting access to the following resource:
|
||||||
is requesting access to the following resource:
|
</div>
|
||||||
</span>
|
<div className="">
|
||||||
<div className="mb-2 mt-4 border-l border-blue-500 bg-blue-500/20 px-3 py-2 text-mineshaft-200">
|
<div className="mb-2 mt-4 text-mineshaft-200">
|
||||||
<div className="mb-1 lowercase">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<span className="font-bold capitalize">Requested path: </span>
|
<div>
|
||||||
<Badge>{accessDetails.env + accessDetails.secretPath || ""}</Badge>
|
<div className="mb-1 text-xs font-semibold uppercase">Environment</div>
|
||||||
</div>
|
<div>{accessDetails.env || "-"}</div>
|
||||||
|
|
||||||
<div className="mb-1">
|
|
||||||
<span className="font-bold">Permissions: </span>
|
|
||||||
<Badge>{requestedAccess}</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span className="font-bold">Access Type: </span>
|
|
||||||
<span>{getAccessLabel()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{request.note && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<span className="font-bold">User Note: </span>
|
|
||||||
<span>{request.note}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase">Secret Path</div>
|
||||||
|
<div>{accessDetails.secretPath || "-"}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase">Access Type</div>
|
||||||
|
<div>{getAccessLabel()}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase">Permission</div>
|
||||||
|
<div>{requestedAccess}</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase">Note</div>
|
||||||
|
<div>{request.note || "-"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-x-2">
|
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
||||||
<Button
|
<div className="thin-scrollbar max-h-64 overflow-y-auto rounded p-2">
|
||||||
isLoading={isLoading === "approved"}
|
{approverSequence?.approvers?.map((approver, index) => (
|
||||||
isDisabled={
|
<div
|
||||||
!!isLoading ||
|
key={`approval-list-${index + 1}`}
|
||||||
(!(
|
className={twMerge(
|
||||||
request.isApprover &&
|
"relative mb-2 flex items-center rounded border border-mineshaft-500 bg-mineshaft-700 p-4",
|
||||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
approverSequence?.currentSequence !== approver.sequence &&
|
||||||
) &&
|
!hasApproved &&
|
||||||
!bypassApproval)
|
"text-mineshaft-400"
|
||||||
}
|
|
||||||
onClick={() => handleReview("approved")}
|
|
||||||
className="mt-4"
|
|
||||||
size="sm"
|
|
||||||
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
|
||||||
>
|
|
||||||
Approve Request
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading === "rejected"}
|
|
||||||
isDisabled={!!isLoading}
|
|
||||||
onClick={() => handleReview("rejected")}
|
|
||||||
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Reject Request
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{isSoftEnforcement &&
|
|
||||||
request.isRequestedByCurrentUser &&
|
|
||||||
!(request.isApprover && request.isSelfApproveAllowed) &&
|
|
||||||
canBypass && (
|
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
|
||||||
<Checkbox
|
|
||||||
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
|
||||||
isChecked={bypassApproval}
|
|
||||||
id="byPassApproval"
|
|
||||||
checkIndicatorBg="text-white"
|
|
||||||
className={twMerge(
|
|
||||||
"mr-2",
|
|
||||||
bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-xs text-red">
|
|
||||||
Approve without waiting for requirements to be met (bypass policy protection)
|
|
||||||
</span>
|
|
||||||
</Checkbox>
|
|
||||||
{bypassApproval && (
|
|
||||||
<FormControl
|
|
||||||
label="Reason for bypass"
|
|
||||||
className="mt-2"
|
|
||||||
isRequired
|
|
||||||
tooltipText="Enter a reason for bypassing the secret change policy"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={bypassReason}
|
|
||||||
onChange={(e) => setBypassReason(e.currentTarget.value)}
|
|
||||||
placeholder="Enter reason for bypass (min 10 chars)"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"mr-8 flex h-8 w-8 items-center justify-center text-3xl font-medium",
|
||||||
|
approver.hasApproved && "border-green-400 text-green-400",
|
||||||
|
approver.hasRejected && "border-red-500 text-red-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
{index !== (approverSequence?.approvers?.length || 0) - 1 && (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"absolute bottom-0 left-8 h-5 border-r-2 border-gray-400",
|
||||||
|
approver.hasApproved && "border-green-400",
|
||||||
|
approver.hasRejected && "border-red-500"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{index !== 0 && (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"absolute left-8 top-0 h-5 border-r-2 border-gray-400",
|
||||||
|
approver.hasApproved && "border-green-400",
|
||||||
|
approver.hasRejected && "border-red-500"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid flex-grow grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
||||||
|
<div>
|
||||||
|
{approver?.user
|
||||||
|
?.map(
|
||||||
|
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
||||||
|
)
|
||||||
|
.join(",") || "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
||||||
|
<div>
|
||||||
|
{approver?.group
|
||||||
|
?.map(
|
||||||
|
(el) =>
|
||||||
|
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
|
||||||
|
)
|
||||||
|
.join(",") || "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
||||||
|
<div>{approver.approvals || "-"}</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-16">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<FontAwesomeIcon icon={faUsers} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent hideCloseBtn className="pt-3">
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
||||||
|
<div className="thin-scrollbar flex max-h-64 flex-col gap-1 overflow-y-auto rounded">
|
||||||
|
{approver.reviewers.map((el, idx) => (
|
||||||
|
<div
|
||||||
|
key={`reviewer-${idx + 1}`}
|
||||||
|
className="flex items-center gap-2 bg-mineshaft-700 p-1 text-sm"
|
||||||
|
>
|
||||||
|
<div className="flex-grow">{el.username}</div>
|
||||||
|
<Tooltip
|
||||||
|
content={`Status: ${el?.status || ApprovalStatus.PENDING}`}
|
||||||
|
>
|
||||||
|
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{approverSequence.isMyReviewInThisSequence &&
|
||||||
|
request.status === ApprovalStatus.PENDING && (
|
||||||
|
<div className="mb-4 rounded-r border-l-2 border-l-primary-400 bg-mineshaft-300/5 px-4 py-2.5 text-sm">
|
||||||
|
Awaiting review from you.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{shouldBlockRequestActions ? (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"mb-4 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
||||||
|
isReviewedByMe && "border-l-green-400",
|
||||||
|
!approverSequence.isMyReviewInThisSequence && "border-l-primary-400",
|
||||||
|
hasRejected && "border-l-red-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderCompletedMessages()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "approved"}
|
||||||
|
isDisabled={
|
||||||
|
Boolean(isLoading) ||
|
||||||
|
(!(
|
||||||
|
request.isApprover &&
|
||||||
|
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||||
|
) &&
|
||||||
|
!bypassApproval)
|
||||||
|
}
|
||||||
|
onClick={() => handleReview("approved")}
|
||||||
|
className="mt-4"
|
||||||
|
size="sm"
|
||||||
|
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
||||||
|
>
|
||||||
|
Approve Request
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "rejected"}
|
||||||
|
isDisabled={
|
||||||
|
!!isLoading ||
|
||||||
|
(!(
|
||||||
|
request.isApprover &&
|
||||||
|
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||||
|
) &&
|
||||||
|
!bypassApproval)
|
||||||
|
}
|
||||||
|
onClick={() => handleReview("rejected")}
|
||||||
|
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Reject Request
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isSoftEnforcement &&
|
||||||
|
request.isRequestedByCurrentUser &&
|
||||||
|
!(request.isApprover && request.isSelfApproveAllowed) &&
|
||||||
|
canBypass && (
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
<Checkbox
|
||||||
|
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
||||||
|
isChecked={bypassApproval}
|
||||||
|
id="byPassApproval"
|
||||||
|
checkIndicatorBg="text-white"
|
||||||
|
className={twMerge(
|
||||||
|
"mr-2",
|
||||||
|
bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-red">
|
||||||
|
Approve without waiting for requirements to be met (bypass policy
|
||||||
|
protection)
|
||||||
|
</span>
|
||||||
|
</Checkbox>
|
||||||
|
{bypassApproval && (
|
||||||
|
<FormControl
|
||||||
|
label="Reason for bypass"
|
||||||
|
className="mt-2"
|
||||||
|
isRequired
|
||||||
|
tooltipText="Enter a reason for bypassing the secret change policy"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={bypassReason}
|
||||||
|
onChange={(e) => setBypassReason(e.currentTarget.value)}
|
||||||
|
placeholder="Enter reason for bypass (min 10 chars)"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -188,9 +188,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Environment</Th>
|
<Th>Environment</Th>
|
||||||
<Th>Secret Path</Th>
|
<Th>Secret Path</Th>
|
||||||
<Th className="w-[18%]">Eligible Approvers</Th>
|
|
||||||
<Th className="w-[18%]">Eligible Group Approvers</Th>
|
|
||||||
<Th>Approval Required</Th>
|
|
||||||
<Th>
|
<Th>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -8,12 +11,15 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
FilterableSelect,
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Switch
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { getMemberLabel } from "@app/helpers/members";
|
import { getMemberLabel } from "@app/helpers/members";
|
||||||
@@ -28,6 +34,7 @@ import {
|
|||||||
useUpdateAccessApprovalPolicy
|
useUpdateAccessApprovalPolicy
|
||||||
} from "@app/hooks/api/accessApproval";
|
} from "@app/hooks/api/accessApproval";
|
||||||
import {
|
import {
|
||||||
|
Approver,
|
||||||
ApproverType,
|
ApproverType,
|
||||||
BypasserType,
|
BypasserType,
|
||||||
TAccessApprovalPolicy
|
TAccessApprovalPolicy
|
||||||
@@ -68,10 +75,28 @@ const formSchema = z
|
|||||||
.default([]),
|
.default([]),
|
||||||
policyType: z.nativeEnum(PolicyType),
|
policyType: z.nativeEnum(PolicyType),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||||
allowedSelfApprovals: z.boolean().default(true)
|
allowedSelfApprovals: z.boolean().default(true),
|
||||||
|
sequenceApprovers: z
|
||||||
|
.object({
|
||||||
|
user: z
|
||||||
|
.object({ type: z.literal(ApproverType.User), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.default([]),
|
||||||
|
group: z
|
||||||
|
.object({ type: z.literal(ApproverType.Group), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.default([]),
|
||||||
|
approvals: z.number().min(1).default(1)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.default([])
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (!(data.groupApprovers.length || data.userApprovers.length)) {
|
if (
|
||||||
|
data.policyType === PolicyType.ChangePolicy &&
|
||||||
|
!(data.groupApprovers.length || data.userApprovers.length)
|
||||||
|
) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
path: ["userApprovers"],
|
path: ["userApprovers"],
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@@ -95,6 +120,9 @@ export const AccessPolicyForm = ({
|
|||||||
projectSlug,
|
projectSlug,
|
||||||
editValues
|
editValues
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||||
|
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||||
|
const modalContainer = useRef<HTMLDivElement>(null);
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -104,7 +132,7 @@ export const AccessPolicyForm = ({
|
|||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
values: editValues
|
values: editValues
|
||||||
? {
|
? ({
|
||||||
...editValues,
|
...editValues,
|
||||||
environment: editValues.environment,
|
environment: editValues.environment,
|
||||||
userApprovers:
|
userApprovers:
|
||||||
@@ -124,15 +152,47 @@ export const AccessPolicyForm = ({
|
|||||||
?.filter((bypasser) => bypasser.type === BypasserType.Group)
|
?.filter((bypasser) => bypasser.type === BypasserType.Group)
|
||||||
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
|
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
|
||||||
approvals: editValues?.approvals,
|
approvals: editValues?.approvals,
|
||||||
allowedSelfApprovals: editValues?.allowedSelfApprovals
|
allowedSelfApprovals: editValues?.allowedSelfApprovals,
|
||||||
}
|
sequenceApprovers: editValues.approvers
|
||||||
: undefined
|
?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0))
|
||||||
|
.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
||||||
|
acc[acc.length - 1][curr.type]?.push(curr);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const approvals = curr.approvals || editValues.approvals;
|
||||||
|
acc.push(
|
||||||
|
curr.type === ApproverType.User
|
||||||
|
? {
|
||||||
|
user: [curr],
|
||||||
|
group: [],
|
||||||
|
sequence: 1,
|
||||||
|
approvals
|
||||||
|
}
|
||||||
|
: { group: [curr], user: [], sequence: 1, approvals }
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as { user: Approver[]; group: Approver[]; sequence?: number; approvals: number }[]
|
||||||
|
)
|
||||||
|
} as TFormSchema)
|
||||||
|
: undefined,
|
||||||
|
defaultValues: {
|
||||||
|
sequenceApprovers: [{ approvals: 1 }]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
const sequenceApproversFieldArray = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "sequenceApprovers"
|
||||||
|
});
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { data: groups } = useListWorkspaceGroups(projectId);
|
const { data: groups } = useListWorkspaceGroups(projectId);
|
||||||
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
const environments = currentWorkspace?.environments || [];
|
||||||
const isEditMode = Boolean(editValues);
|
const isEditMode = Boolean(editValues);
|
||||||
|
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || !isEditMode) reset({});
|
if (!isOpen || !isEditMode) reset({});
|
||||||
@@ -157,6 +217,7 @@ export const AccessPolicyForm = ({
|
|||||||
userApprovers,
|
userApprovers,
|
||||||
groupBypassers,
|
groupBypassers,
|
||||||
userBypassers,
|
userBypassers,
|
||||||
|
sequenceApprovers,
|
||||||
...data
|
...data
|
||||||
}: TFormSchema) => {
|
}: TFormSchema) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
@@ -175,7 +236,15 @@ export const AccessPolicyForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await createAccessApprovalPolicy({
|
await createAccessApprovalPolicy({
|
||||||
...data,
|
...data,
|
||||||
approvers: [...userApprovers, ...groupApprovers],
|
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
||||||
|
approvers.user
|
||||||
|
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
|
||||||
|
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
||||||
|
),
|
||||||
|
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
||||||
|
stepNumber: index + 1,
|
||||||
|
numberOfApprovals: el.approvals
|
||||||
|
})),
|
||||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||||
environment: environment.slug,
|
environment: environment.slug,
|
||||||
projectSlug
|
projectSlug
|
||||||
@@ -201,6 +270,7 @@ export const AccessPolicyForm = ({
|
|||||||
groupApprovers,
|
groupApprovers,
|
||||||
userBypassers,
|
userBypassers,
|
||||||
groupBypassers,
|
groupBypassers,
|
||||||
|
sequenceApprovers,
|
||||||
...data
|
...data
|
||||||
}: TFormSchema) => {
|
}: TFormSchema) => {
|
||||||
if (!projectId || !projectSlug) return;
|
if (!projectId || !projectSlug) return;
|
||||||
@@ -221,7 +291,15 @@ export const AccessPolicyForm = ({
|
|||||||
await updateAccessApprovalPolicy({
|
await updateAccessApprovalPolicy({
|
||||||
id: editValues?.id,
|
id: editValues?.id,
|
||||||
...data,
|
...data,
|
||||||
approvers: [...userApprovers, ...groupApprovers],
|
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
||||||
|
approvers.user
|
||||||
|
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
|
||||||
|
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
||||||
|
),
|
||||||
|
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
||||||
|
stepNumber: index + 1,
|
||||||
|
numberOfApprovals: el.approvals
|
||||||
|
})),
|
||||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||||
environment: environment.slug,
|
environment: environment.slug,
|
||||||
projectSlug
|
projectSlug
|
||||||
@@ -285,16 +363,45 @@ export const AccessPolicyForm = ({
|
|||||||
[groups]
|
[groups]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDragStart = (_: React.DragEvent, index: number) => {
|
||||||
|
setDraggedItem(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent, index: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragOverItem(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (draggedItem === null || dragOverItem === null || draggedItem === dragOverItem) {
|
||||||
|
setDraggedItem(null);
|
||||||
|
setDragOverItem(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sequenceApproversFieldArray.move(draggedItem, dragOverItem);
|
||||||
|
|
||||||
|
setDraggedItem(null);
|
||||||
|
setDragOverItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
setDraggedItem(null);
|
||||||
|
setDragOverItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
className="max-w-2xl"
|
className="max-w-3xl"
|
||||||
bodyClassName="overflow-visible"
|
ref={modalContainer}
|
||||||
title={isEditMode ? `Edit ${policyName}` : "Create Policy"}
|
title={isEditMode ? `Edit ${policyName}` : "Create Policy"}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<div className="grid grid-cols-2 gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="policyType"
|
name="policyType"
|
||||||
@@ -306,6 +413,7 @@ export const AccessPolicyForm = ({
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path."
|
tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path."
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
className="flex-grow"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={isEditMode}
|
isDisabled={isEditMode}
|
||||||
@@ -324,25 +432,30 @@ export const AccessPolicyForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
{!isAccessPolicyType && (
|
||||||
control={control}
|
<Controller
|
||||||
name="approvals"
|
control={control}
|
||||||
defaultValue={1}
|
name="approvals"
|
||||||
render={({ field, fieldState: { error } }) => (
|
defaultValue={1}
|
||||||
<FormControl
|
render={({ field, fieldState: { error } }) => (
|
||||||
label="Minimum Approvals Required"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
label="Min. Approvals Required"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Input
|
className="flex-grow"
|
||||||
{...field}
|
>
|
||||||
type="number"
|
<Input
|
||||||
min={1}
|
{...field}
|
||||||
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
type="number"
|
||||||
/>
|
min={1}
|
||||||
</FormControl>
|
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
||||||
)}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-3">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -351,6 +464,7 @@ export const AccessPolicyForm = ({
|
|||||||
label="Policy Name"
|
label="Policy Name"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
className="flex-grow"
|
||||||
>
|
>
|
||||||
<Input {...field} value={field.value || ""} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -366,6 +480,7 @@ export const AccessPolicyForm = ({
|
|||||||
label="Secret Path"
|
label="Secret Path"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
className="flex-grow"
|
||||||
>
|
>
|
||||||
<Input {...field} value={field.value || ""} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -400,62 +515,199 @@ export const AccessPolicyForm = ({
|
|||||||
Select members or groups that are allowed to approve requests from this policy.
|
Select members or groups that are allowed to approve requests from this policy.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
{isAccessPolicyType ? (
|
||||||
<Controller
|
<>
|
||||||
control={control}
|
<div className="thin-scrollbar max-h-64 space-y-2 overflow-y-auto rounded">
|
||||||
name="userApprovers"
|
{sequenceApproversFieldArray.fields.map((el, index) => (
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
<div
|
||||||
<FormControl
|
className={twMerge(
|
||||||
label="User Approvers"
|
"rounded border border-mineshaft-500 bg-mineshaft-700 p-3 pb-0",
|
||||||
isError={Boolean(error)}
|
dragOverItem === index ? "border-2 border-blue-400" : "",
|
||||||
errorText={error?.message}
|
draggedItem === index ? "opacity-50" : ""
|
||||||
className="w-1/2"
|
)}
|
||||||
>
|
key={el.id}
|
||||||
<FilterableSelect
|
onDragOver={(e) => handleDragOver(e, index)}
|
||||||
menuPlacement="top"
|
onDrop={handleDrop}
|
||||||
isMulti
|
>
|
||||||
placeholder="Select members..."
|
<div className="mb-3 flex items-center justify-between">
|
||||||
options={memberOptions}
|
<Tag>Step {index + 1}</Tag>
|
||||||
getOptionValue={(option) => option.id}
|
<div className="flex items-center gap-3">
|
||||||
getOptionLabel={(option) => {
|
<div className="inline text-xs text-mineshaft-400">Min. Approvals</div>
|
||||||
const member = members?.find((m) => m.user.id === option.id);
|
<div className="mr-2 w-20 border-r border-mineshaft-400 pr-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`sequenceApprovers.${index}.approvals` as const}
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
size="xs"
|
||||||
|
min={1}
|
||||||
|
onChange={(val) => field.onChange(parseInt(val.target.value, 10))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Tooltip content="Remove step">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => sequenceApproversFieldArray.remove(index)}
|
||||||
|
className="text-red-500 hover:text-gray-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Drag to reorder permission">
|
||||||
|
<div
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, index)}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
className="mr-2 cursor-move text-gray-400 hover:text-gray-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faGripVertical} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`sequenceApprovers.${index}.user` as const}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="User Approvers"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="flex-grow"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPortalTarget={modalContainer.current}
|
||||||
|
menuPlacement="top"
|
||||||
|
isMulti
|
||||||
|
placeholder="Select members..."
|
||||||
|
options={memberOptions}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) => {
|
||||||
|
const member = members?.find((m) => m.user.id === option.id);
|
||||||
|
|
||||||
if (!member) return option.id;
|
if (!member) return option.id;
|
||||||
|
|
||||||
return getMemberLabel(member);
|
return getMemberLabel(member);
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="groupApprovers"
|
name={`sequenceApprovers.${index}.group` as const}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Group Approvers"
|
label="Group Approvers"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
className="w-1/2"
|
className="flex-grow"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPortalTarget={modalContainer.current}
|
||||||
|
menuPlacement="top"
|
||||||
|
isMulti
|
||||||
|
placeholder="Select groups..."
|
||||||
|
options={groupOptions}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) =>
|
||||||
|
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
||||||
|
option.id
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="my-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() =>
|
||||||
|
sequenceApproversFieldArray.append({
|
||||||
|
approvals: 1,
|
||||||
|
user: [],
|
||||||
|
group: []
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
Add Step
|
||||||
menuPlacement="top"
|
</Button>
|
||||||
isMulti
|
</div>
|
||||||
placeholder="Select groups..."
|
</>
|
||||||
options={groupOptions}
|
) : (
|
||||||
getOptionValue={(option) => option.id}
|
<div className="flex gap-2">
|
||||||
getOptionLabel={(option) =>
|
<Controller
|
||||||
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
control={control}
|
||||||
}
|
name="userApprovers"
|
||||||
value={value}
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
onChange={onChange}
|
<FormControl
|
||||||
/>
|
label="User Approvers"
|
||||||
</FormControl>
|
isError={Boolean(error)}
|
||||||
)}
|
errorText={error?.message}
|
||||||
/>
|
className="w-1/2"
|
||||||
</div>
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
|
isMulti
|
||||||
|
placeholder="Select members..."
|
||||||
|
options={memberOptions}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) => {
|
||||||
|
const member = members?.find((m) => m.user.id === option.id);
|
||||||
|
|
||||||
|
if (!member) return option.id;
|
||||||
|
|
||||||
|
return getMemberLabel(member);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="groupApprovers"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Group Approvers"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="w-1/2"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
|
isMulti
|
||||||
|
placeholder="Select groups..."
|
||||||
|
options={groupOptions}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) =>
|
||||||
|
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
||||||
|
option.id
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="allowedSelfApprovals"
|
name="allowedSelfApprovals"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
Td,
|
Td,
|
||||||
Tooltip,
|
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
@@ -18,6 +17,7 @@ import { ProjectPermissionSub } from "@app/context";
|
|||||||
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { getMemberLabel } from "@app/helpers/members";
|
import { getMemberLabel } from "@app/helpers/members";
|
||||||
import { policyDetails } from "@app/helpers/policies";
|
import { policyDetails } from "@app/helpers/policies";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
import { Approver } from "@app/hooks/api/accessApproval/types";
|
import { Approver } from "@app/hooks/api/accessApproval/types";
|
||||||
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
||||||
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
|
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
|
||||||
@@ -53,113 +53,154 @@ export const ApprovalPolicyRow = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete
|
onDelete
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useToggle();
|
||||||
|
|
||||||
const labels = useMemo(() => {
|
const labels = useMemo(() => {
|
||||||
const usersInPolicy = policy.approvers
|
const sortedSteps = policy.approvers?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0));
|
||||||
?.filter((approver) => approver.type === ApproverType.User)
|
const entityInSameSequence = sortedSteps?.reduce(
|
||||||
.map((approver) => approver.id);
|
(acc, curr) => {
|
||||||
|
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
||||||
|
acc[acc.length - 1][curr.type]?.push(curr);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const approvals = curr.approvals || policy.approvals;
|
||||||
|
acc.push(
|
||||||
|
curr.type === ApproverType.User
|
||||||
|
? { user: [curr], group: [], sequence: 1, approvals }
|
||||||
|
: { group: [curr], user: [], sequence: 1, approvals }
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as { user: Approver[]; group: Approver[]; sequence?: number; approvals: number }[]
|
||||||
|
);
|
||||||
|
|
||||||
const groupsInPolicy = policy.approvers
|
return entityInSameSequence?.map((el) => {
|
||||||
?.filter((approver) => approver.type === ApproverType.Group)
|
return {
|
||||||
.map((approver) => approver.id);
|
sequence: el.sequence || policy.approvals,
|
||||||
|
userLabels: members
|
||||||
const memberLabels = usersInPolicy?.length
|
?.filter((member) => el.user.find((i) => i.id === member.user.id))
|
||||||
? members
|
|
||||||
.filter((member) => usersInPolicy?.includes(member.user.id))
|
|
||||||
.map((member) => getMemberLabel(member))
|
.map((member) => getMemberLabel(member))
|
||||||
.join(", ")
|
.join(","),
|
||||||
: null;
|
groupLabels: groups
|
||||||
|
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
|
||||||
const groupLabels = groupsInPolicy?.length
|
|
||||||
? groups
|
|
||||||
.filter(({ group }) => groupsInPolicy?.includes(group.id))
|
|
||||||
.map(({ group }) => group.name)
|
.map(({ group }) => group.name)
|
||||||
.join(", ")
|
.join(","),
|
||||||
: null;
|
approvals: el.approvals
|
||||||
|
};
|
||||||
return {
|
});
|
||||||
members: memberLabels,
|
|
||||||
groups: groupLabels
|
|
||||||
};
|
|
||||||
}, [policy, members, groups]);
|
}, [policy, members, groups]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr>
|
<>
|
||||||
<Td>{policy.name}</Td>
|
<Tr
|
||||||
<Td>{policy.environment.slug}</Td>
|
isHoverable
|
||||||
<Td>{policy.secretPath || "*"}</Td>
|
isSelectable
|
||||||
<Td className="max-w-0">
|
role="button"
|
||||||
<Tooltip
|
tabIndex={0}
|
||||||
side="left"
|
onKeyDown={(evt) => {
|
||||||
content={labels.members ?? "No users are assigned as approvers for this policy"}
|
if (evt.key === "Enter") setIsExpanded.toggle();
|
||||||
>
|
}}
|
||||||
<p className="truncate">{labels.members ?? "-"}</p>
|
onClick={() => setIsExpanded.toggle()}
|
||||||
</Tooltip>
|
>
|
||||||
</Td>
|
<Td>{policy.name}</Td>
|
||||||
<Td className="max-w-0">
|
<Td>{policy.environment.slug}</Td>
|
||||||
<Tooltip
|
<Td>{policy.secretPath || "*"}</Td>
|
||||||
side="left"
|
<Td>
|
||||||
content={labels.groups ?? "No groups are assigned as approvers for this policy"}
|
<Badge className={policyDetails[policy.policyType].className}>
|
||||||
>
|
{policyDetails[policy.policyType].name}
|
||||||
<p className="truncate">{labels.groups ?? "-"}</p>
|
</Badge>
|
||||||
</Tooltip>
|
</Td>
|
||||||
</Td>
|
<Td>
|
||||||
<Td>{policy.approvals}</Td>
|
<DropdownMenu>
|
||||||
<Td>
|
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
||||||
<Badge className={policyDetails[policy.policyType].className}>
|
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
||||||
{policyDetails[policy.policyType].name}
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
</Badge>
|
</div>
|
||||||
</Td>
|
</DropdownMenuTrigger>
|
||||||
<Td>
|
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
||||||
<DropdownMenu>
|
<ProjectPermissionCan
|
||||||
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
I={ProjectPermissionActions.Edit}
|
||||||
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
>
|
||||||
</div>
|
{(isAllowed) => (
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuItem
|
||||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
className={twMerge(
|
||||||
<ProjectPermissionCan
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
I={ProjectPermissionActions.Edit}
|
)}
|
||||||
a={ProjectPermissionSub.SecretApproval}
|
onClick={(e) => {
|
||||||
>
|
e.stopPropagation();
|
||||||
{(isAllowed) => (
|
onEdit();
|
||||||
<DropdownMenuItem
|
}}
|
||||||
className={twMerge(
|
disabled={!isAllowed}
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
>
|
||||||
|
Edit Policy
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
isAllowed
|
||||||
|
? "hover:!bg-red-500 hover:!text-white"
|
||||||
|
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Delete Policy
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{isExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5} className="rounded bg-mineshaft-900">
|
||||||
|
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
||||||
|
{labels?.map((el, index) => (
|
||||||
|
<div
|
||||||
|
key={`approval-list-${index + 1}`}
|
||||||
|
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-700 p-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="mr-8 flex h-8 w-8 items-center justify-center border border-bunker-300 bg-bunker-800 text-white">
|
||||||
|
<div className="text-lg">{index + 1}</div>
|
||||||
|
</div>
|
||||||
|
{index !== labels.length - 1 && (
|
||||||
|
<div className="absolute bottom-0 left-8 h-6 border-r border-gray-400" />
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
{index !== 0 && (
|
||||||
e.stopPropagation();
|
<div className="absolute left-8 top-0 h-4 border-r border-gray-400" />
|
||||||
onEdit();
|
|
||||||
}}
|
|
||||||
disabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Edit Policy
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Delete}
|
|
||||||
a={ProjectPermissionSub.SecretApproval}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className={twMerge(
|
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
</div>
|
||||||
e.stopPropagation();
|
<div className="grid flex-grow grid-cols-3">
|
||||||
onDelete();
|
<div>
|
||||||
}}
|
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
||||||
disabled={!isAllowed}
|
<div>{el.userLabels || "-"}</div>
|
||||||
>
|
</div>
|
||||||
Delete Policy
|
<div>
|
||||||
</DropdownMenuItem>
|
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
||||||
)}
|
<div>{el.groupLabels || "-"}</div>
|
||||||
</ProjectPermissionCan>
|
</div>
|
||||||
</DropdownMenuContent>
|
<div>
|
||||||
</DropdownMenu>
|
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
||||||
</Td>
|
<div>{el.approvals || "-"}</div>
|
||||||
</Tr>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||