mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 13:26:31 +00:00
Allow project approval workflows to set if a reviewer can or can not review their own requests
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas/models";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "selfApprovals"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
t.boolean("selfApprovals").notNullable().defaultTo(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "selfApprovals"))) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||||
|
t.boolean("selfApprovals").notNullable().defaultTo(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "selfApprovals")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
t.dropColumn("selfApprovals");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "selfApprovals")) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||||
|
t.dropColumn("selfApprovals");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ export const AccessApprovalPoliciesSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
enforcementLevel: z.string().default("hard"),
|
enforcementLevel: z.string().default("hard"),
|
||||||
deletedAt: z.date().nullable().optional()
|
deletedAt: z.date().nullable().optional(),
|
||||||
|
selfApprovals: z.boolean().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const SecretApprovalPoliciesSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
enforcementLevel: z.string().default("hard"),
|
enforcementLevel: z.string().default("hard"),
|
||||||
deletedAt: z.date().nullable().optional()
|
deletedAt: z.date().nullable().optional(),
|
||||||
|
selfApprovals: z.boolean().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.array()
|
.array()
|
||||||
.min(1, { message: "At least one approver should be provided" }),
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
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),
|
||||||
|
selfApprovals: z.boolean().default(true)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -147,7 +148,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.array()
|
.array()
|
||||||
.min(1, { message: "At least one approver should be provided" }),
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
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),
|
||||||
|
selfApprovals: z.boolean().default(true)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
secretPath: z.string().nullish(),
|
secretPath: z.string().nullish(),
|
||||||
envId: z.string(),
|
envId: z.string(),
|
||||||
enforcementLevel: z.string(),
|
enforcementLevel: z.string(),
|
||||||
deletedAt: z.date().nullish()
|
deletedAt: z.date().nullish(),
|
||||||
|
selfApprovals: z.boolean()
|
||||||
}),
|
}),
|
||||||
reviewers: z
|
reviewers: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.array()
|
.array()
|
||||||
.min(1, { message: "At least one approver should be provided" }),
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
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),
|
||||||
|
selfApprovals: z.boolean().default(true)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -85,7 +86,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.nullable()
|
.nullable()
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
.transform((val) => (val === "" ? "/" : val)),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
|
||||||
|
selfApprovals: z.boolean().default(true)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
.array(),
|
.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
enforcementLevel: z.string(),
|
enforcementLevel: z.string(),
|
||||||
deletedAt: z.date().nullish()
|
deletedAt: z.date().nullish(),
|
||||||
|
selfApprovals: z.boolean()
|
||||||
}),
|
}),
|
||||||
committerUser: approvalRequestUser,
|
committerUser: approvalRequestUser,
|
||||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||||
@@ -267,7 +268,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
approvers: approvalRequestUser.array(),
|
approvers: approvalRequestUser.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
enforcementLevel: z.string(),
|
enforcementLevel: z.string(),
|
||||||
deletedAt: z.date().nullish()
|
deletedAt: z.date().nullish(),
|
||||||
|
selfApprovals: z.boolean()
|
||||||
}),
|
}),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
approvers,
|
approvers,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
environment,
|
environment,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
}: 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` });
|
||||||
@@ -153,7 +154,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -216,7 +218,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
approvals,
|
approvals,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
}: TUpdateAccessApprovalPolicy) => {
|
}: TUpdateAccessApprovalPolicy) => {
|
||||||
const groupApprovers = approvers
|
const groupApprovers = approvers
|
||||||
.filter((approver) => approver.type === ApproverType.Group)
|
.filter((approver) => approver.type === ApproverType.Group)
|
||||||
@@ -262,7 +265,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type TCreateAccessApprovalPolicy = {
|
|||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
|
selfApprovals: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateAccessApprovalPolicy = {
|
export type TUpdateAccessApprovalPolicy = {
|
||||||
@@ -35,6 +36,7 @@ export type TUpdateAccessApprovalPolicy = {
|
|||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
selfApprovals: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteAccessApprovalPolicy = {
|
export type TDeleteAccessApprovalPolicy = {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||||
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
|
db.ref("selfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policySelfApprovals"),
|
||||||
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")
|
||||||
)
|
)
|
||||||
@@ -119,6 +120,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approvals: doc.policyApprovals,
|
approvals: doc.policyApprovals,
|
||||||
secretPath: doc.policySecretPath,
|
secretPath: doc.policySecretPath,
|
||||||
enforcementLevel: doc.policyEnforcementLevel,
|
enforcementLevel: doc.policyEnforcementLevel,
|
||||||
|
selfApprovals: doc.policySelfApprovals,
|
||||||
envId: doc.policyEnvId,
|
envId: doc.policyEnvId,
|
||||||
deletedAt: doc.policyDeletedAt
|
deletedAt: doc.policyDeletedAt
|
||||||
},
|
},
|
||||||
@@ -254,6 +256,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||||
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
|
tx.ref("selfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policySelfApprovals"),
|
||||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||||
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
|
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
|
||||||
);
|
);
|
||||||
@@ -275,6 +278,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPath: el.policySecretPath,
|
||||||
enforcementLevel: el.policyEnforcementLevel,
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
|
selfApprovals: el.policySelfApprovals,
|
||||||
deletedAt: el.policyDeletedAt
|
deletedAt: el.policyDeletedAt
|
||||||
},
|
},
|
||||||
requestedByUser: {
|
requestedByUser: {
|
||||||
|
|||||||
@@ -320,6 +320,11 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
message: "The policy associated with this access request has been deleted."
|
message: "The policy associated with this access request has been deleted."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!policy.selfApprovals && actorId === accessApprovalRequest.requestedByUserId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to review access approval request. Users are not authorized to review their own request."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission({
|
const { membership, hasRole } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
secretPath,
|
secretPath,
|
||||||
environment,
|
environment,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
}: TCreateSapDTO) => {
|
}: TCreateSapDTO) => {
|
||||||
const groupApprovers = approvers
|
const groupApprovers = approvers
|
||||||
?.filter((approver) => approver.type === ApproverType.Group)
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
@@ -113,7 +114,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -172,7 +174,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
approvals,
|
approvals,
|
||||||
secretPolicyId,
|
secretPolicyId,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
}: TUpdateSapDTO) => {
|
}: TUpdateSapDTO) => {
|
||||||
const groupApprovers = approvers
|
const groupApprovers = approvers
|
||||||
?.filter((approver) => approver.type === ApproverType.Group)
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
@@ -218,7 +221,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type TCreateSapDTO = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
|
selfApprovals: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateSapDTO = {
|
export type TUpdateSapDTO = {
|
||||||
@@ -19,6 +20,7 @@ export type TUpdateSapDTO = {
|
|||||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
selfApprovals?: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteSapDTO = {
|
export type TDeleteSapDTO = {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||||
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
|
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
|
||||||
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
|
tx.ref("selfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policySelfApprovals"),
|
||||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt")
|
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt")
|
||||||
);
|
);
|
||||||
@@ -150,7 +151,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
secretPath: el.policySecretPath,
|
secretPath: el.policySecretPath,
|
||||||
enforcementLevel: el.policyEnforcementLevel,
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
envId: el.policyEnvId,
|
envId: el.policyEnvId,
|
||||||
deletedAt: el.policyDeletedAt
|
deletedAt: el.policyDeletedAt,
|
||||||
|
selfApprovals: el.policySelfApprovals
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
@@ -336,6 +338,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
),
|
),
|
||||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
|
db.ref("selfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policySelfApprovals"),
|
||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||||
@@ -364,7 +367,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPath: el.policySecretPath,
|
||||||
enforcementLevel: el.policyEnforcementLevel
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
|
selfApprovals: el.policySelfApprovals
|
||||||
},
|
},
|
||||||
committerUser: {
|
committerUser: {
|
||||||
userId: el.committerUserId,
|
userId: el.committerUserId,
|
||||||
@@ -482,6 +486,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||||
),
|
),
|
||||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||||
|
db.ref("selfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policySelfApprovals"),
|
||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
@@ -511,7 +516,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPath: el.policySecretPath,
|
||||||
enforcementLevel: el.policyEnforcementLevel
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
|
selfApprovals: el.policySelfApprovals
|
||||||
},
|
},
|
||||||
committerUser: {
|
committerUser: {
|
||||||
userId: el.committerUserId,
|
userId: el.committerUserId,
|
||||||
|
|||||||
@@ -352,6 +352,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
message: "The policy associated with this secret approval request has been deleted."
|
message: "The policy associated with this secret approval request has been deleted."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!policy.selfApprovals && actorId === secretApprovalRequest.committerUserId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to review secret approval request. Users are not authorized to review their own request."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { hasRole } = await permissionService.getProjectPermission({
|
const { hasRole } = await permissionService.getProjectPermission({
|
||||||
actor: ActorType.USER,
|
actor: ActorType.USER,
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export const useCreateAccessApprovalPolicy = () => {
|
|||||||
approvers,
|
approvers,
|
||||||
name,
|
name,
|
||||||
secretPath,
|
secretPath,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
||||||
environment,
|
environment,
|
||||||
@@ -32,7 +33,8 @@ export const useCreateAccessApprovalPolicy = () => {
|
|||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -48,13 +50,22 @@ export const useUpdateAccessApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, TUpdateAccessPolicyDTO>({
|
return useMutation<object, object, TUpdateAccessPolicyDTO>({
|
||||||
mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => {
|
mutationFn: async ({
|
||||||
|
id,
|
||||||
|
approvers,
|
||||||
|
approvals,
|
||||||
|
name,
|
||||||
|
secretPath,
|
||||||
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
|
}) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type TAccessApprovalPolicy = {
|
|||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
|
selfApprovals: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ApproverType {
|
export enum ApproverType {
|
||||||
@@ -71,6 +72,7 @@ export type TAccessApprovalRequest = {
|
|||||||
envId: string;
|
envId: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
deletedAt: Date | null;
|
deletedAt: Date | null;
|
||||||
|
selfApprovals: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
reviewers: {
|
reviewers: {
|
||||||
@@ -144,6 +146,7 @@ export type TCreateAccessPolicyDTO = {
|
|||||||
approvals?: number;
|
approvals?: number;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
selfApprovals: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateAccessPolicyDTO = {
|
export type TUpdateAccessPolicyDTO = {
|
||||||
@@ -154,6 +157,7 @@ export type TUpdateAccessPolicyDTO = {
|
|||||||
environment?: string;
|
environment?: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
selfApprovals: boolean;
|
||||||
// for invalidating list
|
// for invalidating list
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const useCreateSecretApprovalPolicy = () => {
|
|||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
||||||
environment,
|
environment,
|
||||||
@@ -25,7 +26,8 @@ export const useCreateSecretApprovalPolicy = () => {
|
|||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -41,13 +43,22 @@ export const useUpdateSecretApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, TUpdateSecretPolicyDTO>({
|
return useMutation<object, object, TUpdateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ id, approvers, approvals, secretPath, name, enforcementLevel }) => {
|
mutationFn: async ({
|
||||||
|
id,
|
||||||
|
approvers,
|
||||||
|
approvals,
|
||||||
|
secretPath,
|
||||||
|
name,
|
||||||
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
|
}) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel,
|
||||||
|
selfApprovals
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type TSecretApprovalPolicy = {
|
|||||||
approvers: Approver[];
|
approvers: Approver[];
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
|
selfApprovals: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ApproverType {
|
export enum ApproverType {
|
||||||
@@ -42,6 +43,7 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
|
selfApprovals: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateSecretPolicyDTO = {
|
export type TUpdateSecretPolicyDTO = {
|
||||||
@@ -50,6 +52,7 @@ export type TUpdateSecretPolicyDTO = {
|
|||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
|
selfApprovals?: boolean;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
// for invalidating list
|
// for invalidating list
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|||||||
+10
-8
@@ -152,7 +152,7 @@ export const AccessApprovalRequest = ({
|
|||||||
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.selfApprovals;
|
||||||
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
|
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
|
||||||
|
|
||||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
||||||
@@ -189,7 +189,8 @@ export const AccessApprovalRequest = ({
|
|||||||
userReviewStatus,
|
userReviewStatus,
|
||||||
isAccepted,
|
isAccepted,
|
||||||
isSoftEnforcement,
|
isSoftEnforcement,
|
||||||
isRequestedByCurrentUser
|
isRequestedByCurrentUser,
|
||||||
|
isSelfApproveAllowed
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -342,15 +343,16 @@ export const AccessApprovalRequest = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
(!details.isApprover ||
|
((!details.isApprover ||
|
||||||
details.isReviewedByUser ||
|
details.isReviewedByUser ||
|
||||||
details.isRejectedByAnyone ||
|
details.isRejectedByAnyone ||
|
||||||
details.isAccepted) &&
|
details.isAccepted) &&
|
||||||
!(
|
!(
|
||||||
details.isSoftEnforcement &&
|
details.isSoftEnforcement &&
|
||||||
details.isRequestedByCurrentUser &&
|
details.isRequestedByCurrentUser &&
|
||||||
!details.isAccepted
|
!details.isAccepted
|
||||||
)
|
)) ||
|
||||||
|
(request.requestedByUserId === user.id && !details.isSelfApproveAllowed)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
if (membersGroupById?.[request.requestedByUserId].user) {
|
if (membersGroupById?.[request.requestedByUserId].user) {
|
||||||
|
|||||||
+27
-3
@@ -12,7 +12,8 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
|
Switch
|
||||||
} 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";
|
||||||
@@ -54,7 +55,8 @@ const formSchema = z
|
|||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
policyType: z.nativeEnum(PolicyType),
|
policyType: z.nativeEnum(PolicyType),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
enforcementLevel: z.nativeEnum(EnforcementLevel),
|
||||||
|
selfApprovals: z.boolean().default(true)
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (!(data.groupApprovers.length || data.userApprovers.length)) {
|
if (!(data.groupApprovers.length || data.userApprovers.length)) {
|
||||||
@@ -101,7 +103,8 @@ export const AccessPolicyForm = ({
|
|||||||
editValues?.approvers
|
editValues?.approvers
|
||||||
?.filter((approver) => approver.type === ApproverType.Group)
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
.map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [],
|
.map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [],
|
||||||
approvals: editValues?.approvals
|
approvals: editValues?.approvals,
|
||||||
|
selfApprovals: editValues?.selfApprovals
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
});
|
});
|
||||||
@@ -441,6 +444,27 @@ export const AccessPolicyForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="selfApprovals"
|
||||||
|
defaultValue
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Self Approvals"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="self-approvals"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
isChecked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
>
|
||||||
|
Allow approvers to review their own requests
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div className="mt-8 flex items-center space-x-4">
|
<div className="mt-8 flex items-center space-x-4">
|
||||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
|
|||||||
+152
-143
@@ -127,7 +127,9 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
} = useForm<TReviewFormSchema>({
|
} = useForm<TReviewFormSchema>({
|
||||||
resolver: zodResolver(reviewFormSchema)
|
resolver: zodResolver(reviewFormSchema)
|
||||||
});
|
});
|
||||||
|
const shouldBlockSelfReview =
|
||||||
|
secretApprovalRequestDetails?.policy?.selfApprovals === false &&
|
||||||
|
secretApprovalRequestDetails?.committerUserId === userSession.id;
|
||||||
const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus;
|
const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus;
|
||||||
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
||||||
|
|
||||||
@@ -245,117 +247,119 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!hasMerged && secretApprovalRequestDetails.status === "open" && (
|
{!hasMerged &&
|
||||||
<DropdownMenu
|
secretApprovalRequestDetails.status === "open" &&
|
||||||
open={popUp.reviewChanges.isOpen}
|
!shouldBlockSelfReview && (
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
|
<DropdownMenu
|
||||||
>
|
open={popUp.reviewChanges.isOpen}
|
||||||
<DropdownMenuTrigger asChild>
|
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
|
||||||
<Button
|
>
|
||||||
variant="outline_bg"
|
<DropdownMenuTrigger asChild>
|
||||||
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
<Button
|
||||||
>
|
variant="outline_bg"
|
||||||
Review
|
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
||||||
</Button>
|
>
|
||||||
</DropdownMenuTrigger>
|
Review
|
||||||
<DropdownMenuContent align="end" asChild className="mt-3">
|
</Button>
|
||||||
<form onSubmit={handleSubmit(handleSubmitReview)}>
|
</DropdownMenuTrigger>
|
||||||
<div className="flex w-[400px] flex-col space-y-2 p-5">
|
<DropdownMenuContent align="end" asChild className="mt-3">
|
||||||
<div className="text-lg font-medium">Finish your review</div>
|
<form onSubmit={handleSubmit(handleSubmitReview)}>
|
||||||
<Controller
|
<div className="flex w-[400px] flex-col space-y-2 p-5">
|
||||||
control={control}
|
<div className="text-lg font-medium">Finish your review</div>
|
||||||
name="comment"
|
<Controller
|
||||||
render={({ field, fieldState: { error } }) => (
|
control={control}
|
||||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
name="comment"
|
||||||
<TextArea
|
render={({ field, fieldState: { error } }) => (
|
||||||
{...field}
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
placeholder="Leave a comment..."
|
<TextArea
|
||||||
reSize="none"
|
{...field}
|
||||||
className="text-md mt-2 h-48 border border-mineshaft-600 bg-bunker-800"
|
placeholder="Leave a comment..."
|
||||||
/>
|
reSize="none"
|
||||||
</FormControl>
|
className="text-md mt-2 h-48 border border-mineshaft-600 bg-bunker-800"
|
||||||
)}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
<Controller
|
)}
|
||||||
control={control}
|
/>
|
||||||
name="status"
|
<Controller
|
||||||
defaultValue={ApprovalStatus.APPROVED}
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="status"
|
||||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
defaultValue={ApprovalStatus.APPROVED}
|
||||||
<RadioGroup
|
render={({ field, fieldState: { error } }) => (
|
||||||
value={field.value}
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
onValueChange={field.onChange}
|
<RadioGroup
|
||||||
className="mb-4 space-y-2"
|
value={field.value}
|
||||||
aria-label="Status"
|
onValueChange={field.onChange}
|
||||||
>
|
className="mb-4 space-y-2"
|
||||||
<div className="flex items-center gap-2">
|
aria-label="Status"
|
||||||
<RadioGroupItem
|
>
|
||||||
id="approve"
|
<div className="flex items-center gap-2">
|
||||||
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
<RadioGroupItem
|
||||||
value={ApprovalStatus.APPROVED}
|
id="approve"
|
||||||
aria-labelledby="approve-label"
|
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
||||||
>
|
value={ApprovalStatus.APPROVED}
|
||||||
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
aria-labelledby="approve-label"
|
||||||
</RadioGroupItem>
|
>
|
||||||
<span
|
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
||||||
id="approve-label"
|
</RadioGroupItem>
|
||||||
className="cursor-pointer"
|
<span
|
||||||
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
|
id="approve-label"
|
||||||
onKeyDown={(e) => {
|
className="cursor-pointer"
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
|
||||||
e.preventDefault();
|
onKeyDown={(e) => {
|
||||||
field.onChange(ApprovalStatus.APPROVED);
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
}
|
e.preventDefault();
|
||||||
}}
|
field.onChange(ApprovalStatus.APPROVED);
|
||||||
tabIndex={0}
|
}
|
||||||
role="button"
|
}}
|
||||||
>
|
tabIndex={0}
|
||||||
Approve
|
role="button"
|
||||||
</span>
|
>
|
||||||
</div>
|
Approve
|
||||||
<div className="flex items-center gap-2">
|
</span>
|
||||||
<RadioGroupItem
|
</div>
|
||||||
id="reject"
|
<div className="flex items-center gap-2">
|
||||||
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
<RadioGroupItem
|
||||||
value={ApprovalStatus.REJECTED}
|
id="reject"
|
||||||
aria-labelledby="reject-label"
|
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
||||||
>
|
value={ApprovalStatus.REJECTED}
|
||||||
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
aria-labelledby="reject-label"
|
||||||
</RadioGroupItem>
|
>
|
||||||
<span
|
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
||||||
id="reject-label"
|
</RadioGroupItem>
|
||||||
className="cursor-pointer"
|
<span
|
||||||
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
|
id="reject-label"
|
||||||
onKeyDown={(e) => {
|
className="cursor-pointer"
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
|
||||||
e.preventDefault();
|
onKeyDown={(e) => {
|
||||||
field.onChange(ApprovalStatus.REJECTED);
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
}
|
e.preventDefault();
|
||||||
}}
|
field.onChange(ApprovalStatus.REJECTED);
|
||||||
tabIndex={0}
|
}
|
||||||
role="button"
|
}}
|
||||||
>
|
tabIndex={0}
|
||||||
Reject
|
role="button"
|
||||||
</span>
|
>
|
||||||
</div>
|
Reject
|
||||||
</RadioGroup>
|
</span>
|
||||||
</FormControl>
|
</div>
|
||||||
)}
|
</RadioGroup>
|
||||||
/>
|
</FormControl>
|
||||||
<div className="flex justify-end">
|
)}
|
||||||
<Button
|
/>
|
||||||
type="submit"
|
<div className="flex justify-end">
|
||||||
isLoading={isApproving || isRejecting || isSubmitting}
|
<Button
|
||||||
variant="outline_bg"
|
type="submit"
|
||||||
>
|
isLoading={isApproving || isRejecting || isSubmitting}
|
||||||
Submit Review
|
variant="outline_bg"
|
||||||
</Button>
|
>
|
||||||
|
Submit Review
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</DropdownMenuContent>
|
||||||
</DropdownMenuContent>
|
</DropdownMenu>
|
||||||
</DropdownMenu>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{secretApprovalRequestDetails.commits.map(
|
{secretApprovalRequestDetails.commits.map(
|
||||||
@@ -422,40 +426,45 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
||||||
<div className="text-sm text-bunker-300">Reviewers</div>
|
<div className="text-sm text-bunker-300">Reviewers</div>
|
||||||
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
||||||
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
{secretApprovalRequestDetails?.policy?.approvers
|
||||||
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
.filter(
|
||||||
return (
|
(requiredApprover) =>
|
||||||
<div
|
!(shouldBlockSelfReview && requiredApprover.userId === userSession.id)
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
)
|
||||||
key={`required-approver-${requiredApprover.userId}`}
|
.map((requiredApprover) => {
|
||||||
>
|
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||||
<div className="flex-grow text-sm">
|
return (
|
||||||
<Tooltip
|
<div
|
||||||
content={`${requiredApprover.firstName || ""} ${
|
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
requiredApprover.lastName || ""
|
key={`required-approver-${requiredApprover.userId}`}
|
||||||
}`}
|
>
|
||||||
>
|
<div className="flex-grow text-sm">
|
||||||
<span>{requiredApprover?.email} </span>
|
<Tooltip
|
||||||
</Tooltip>
|
content={`${requiredApprover.firstName || ""} ${
|
||||||
<span className="text-red">*</span>
|
requiredApprover.lastName || ""
|
||||||
</div>
|
}`}
|
||||||
<div>
|
>
|
||||||
{reviewer?.comment && (
|
<span>{requiredApprover?.email} </span>
|
||||||
<Tooltip content={reviewer.comment}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faComment}
|
|
||||||
size="xs"
|
|
||||||
className="mr-1 text-mineshaft-300"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
<span className="text-red">*</span>
|
||||||
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
|
</div>
|
||||||
{getReviewedStatusSymbol(reviewer?.status)}
|
<div>
|
||||||
</Tooltip>
|
{reviewer?.comment && (
|
||||||
|
<Tooltip content={reviewer.comment}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faComment}
|
||||||
|
size="xs"
|
||||||
|
className="mr-1 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
|
||||||
|
{getReviewedStatusSymbol(reviewer?.status)}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
{secretApprovalRequestDetails?.reviewers
|
{secretApprovalRequestDetails?.reviewers
|
||||||
.filter(
|
.filter(
|
||||||
(reviewer) =>
|
(reviewer) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user