feat: allow access approvals with user groups

This commit is contained in:
Meet
2024-09-20 01:24:30 +05:30
parent 5740d2b4e4
commit 009be0ded8
18 changed files with 457 additions and 33 deletions
@@ -21,6 +21,7 @@ export const useCreateAccessApprovalPolicy = () => {
projectSlug,
approvals,
approvers,
groupApprovers,
name,
secretPath,
enforcementLevel
@@ -30,6 +31,7 @@ export const useCreateAccessApprovalPolicy = () => {
projectSlug,
approvals,
approvers,
groupApprovers,
secretPath,
name,
enforcementLevel
@@ -46,10 +48,11 @@ export const useUpdateAccessApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => {
mutationFn: async ({ id, approvers, groupApprovers, approvals, name, secretPath, enforcementLevel }) => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals,
approvers,
groupApprovers,
secretPath,
name,
enforcementLevel
@@ -17,6 +17,7 @@ export type TAccessApprovalPolicy = {
enforcementLevel: EnforcementLevel;
updatedAt: Date;
userApprovers?: { userId: string }[];
groupApprovers?: { groupId: string }[];
};
export type TAccessApprovalRequest = {
@@ -131,6 +132,7 @@ export type TCreateAccessPolicyDTO = {
name?: string;
environment: string;
approvers?: string[];
groupApprovers?: string[];
approvals?: number;
secretPath?: string;
enforcementLevel?: EnforcementLevel;
@@ -140,6 +142,7 @@ export type TUpdateAccessPolicyDTO = {
id: string;
name?: string;
approvers?: string[];
groupApprovers?: string[];
secretPath?: string;
environment?: string;
approvals?: number;
@@ -41,7 +41,8 @@ import {
useDeleteAccessApprovalPolicy,
useDeleteSecretApprovalPolicy,
useGetSecretApprovalPolicies,
useGetWorkspaceUsers
useGetWorkspaceUsers,
useListWorkspaceGroups
} from "@app/hooks/api";
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
import { PolicyType } from "@app/hooks/api/policies/enums";
@@ -102,6 +103,8 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
const { currentWorkspace } = useWorkspace();
const { data: members } = useGetWorkspaceUsers(workspaceId, true);
const { data: groups } = useListWorkspaceGroups(currentWorkspace?.slug || "");
const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies(
permission,
currentWorkspace
@@ -186,6 +189,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
<Th>Environment</Th>
<Th>Secret Path</Th>
<Th>Eligible Approvers</Th>
<Th>Eligible Group Approvers</Th>
<Th>Approval Required</Th>
<Th>
<DropdownMenu>
@@ -257,6 +261,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
workspaceId={workspaceId}
key={policy.id}
members={members}
groups={groups}
onEdit={() => handlePopUpOpen("policyForm", policy)}
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
/>
@@ -22,7 +22,7 @@ import {
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { policyDetails } from "@app/helpers/policies";
import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
import { useCreateSecretApprovalPolicy, useListWorkspaceGroups, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
import {
useCreateAccessApprovalPolicy,
useUpdateAccessApprovalPolicy
@@ -45,13 +45,14 @@ const formSchema = z
name: z.string().optional(),
secretPath: z.string().optional(),
approvals: z.number().min(1),
approvers: z.string().array().min(1),
approvers: z.string().array().optional(),
groupApprovers: z.string().array().optional(),
policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
.refine((data) => data.approvers || data.groupApprovers, {
path: ["approvers", "groupApprovers"],
message: "At least one approver should be provided."
});
type TFormSchema = z.infer<typeof formSchema>;
@@ -75,11 +76,14 @@ export const AccessPolicyForm = ({
? {
...editValues,
environment: editValues.environment.slug,
approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers,
groupApprovers: editValues?.groupApprovers?.map((group) => group.groupId) || editValues?.groupApprovers,
approvals: editValues?.approvals
}
: undefined
});
const { currentWorkspace } = useWorkspace();
const { data: groups } = useListWorkspaceGroups(projectSlug);
const environments = currentWorkspace?.environments || [];
const isEditMode = Boolean(editValues);
@@ -266,8 +270,7 @@ export const AccessPolicyForm = ({
name="approvers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Required Approvers"
isRequired
label="Required User Approvers"
isError={Boolean(error)}
errorText={error?.message}
>
@@ -312,6 +315,56 @@ export const AccessPolicyForm = ({
</FormControl>
)}
/>
<Controller
control={control}
name="groupApprovers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Required Group Approvers"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value?.length ? `${value.length} selected` : "None"}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>
Select groups that are allowed to approve requests
</DropdownMenuLabel>
{groups && groups.map(({ group }) => {
const { id } = group;
const isChecked = value?.includes(id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== id)
: [...(value || []), id]
);
}}
key={`create-policy-members-${id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{group.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
<Controller
control={control}
name="approvals"
@@ -18,6 +18,7 @@ import { Badge } from "@app/components/v2/Badge";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { policyDetails } from "@app/helpers/policies";
import { useUpdateAccessApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
import { TGroupMembership } from "@app/hooks/api/groups/types";
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { TWorkspaceUser } from "@app/hooks/api/users/types";
@@ -31,6 +32,7 @@ interface IPolicy {
approvals: number;
approvers?: string[];
userApprovers?: { userId: string }[];
groupApprovers?: { groupId: string }[];
updatedAt: Date;
policyType: PolicyType;
enforcementLevel: EnforcementLevel;
@@ -39,6 +41,7 @@ interface IPolicy {
type Props = {
policy: IPolicy;
members?: TWorkspaceUser[];
groups?: TGroupMembership[];
projectSlug: string;
workspaceId: string;
onEdit: () => void;
@@ -48,12 +51,14 @@ type Props = {
export const ApprovalPolicyRow = ({
policy,
members = [],
groups = [],
projectSlug,
workspaceId,
onEdit,
onDelete
}: Props) => {
const [selectedApprovers, setSelectedApprovers] = useState<string[]>(policy.userApprovers?.map(({ userId }) => userId) || policy.approvers || []);
const [selectedGroupApprovers, setSelectedGroupApprovers] = useState<string[]>(policy.groupApprovers?.map(({ groupId }) => groupId) || []);
const { mutate: updateAccessApprovalPolicy, isLoading: isAccessApprovalPolicyLoading } = useUpdateAccessApprovalPolicy();
const { mutate: updateSecretApprovalPolicy, isLoading: isSecretApprovalPolicyLoading } = useUpdateSecretApprovalPolicy();
const isLoading = isAccessApprovalPolicyLoading || isSecretApprovalPolicyLoading;
@@ -74,9 +79,10 @@ export const ApprovalPolicyRow = ({
{
projectSlug,
id: policy.id,
approvers: selectedApprovers
approvers: selectedApprovers,
groupApprovers: selectedGroupApprovers
},
{ onSettled: () => {} }
{ onSettled: () => { } }
);
} else {
updateSecretApprovalPolicy(
@@ -85,7 +91,7 @@ export const ApprovalPolicyRow = ({
id: policy.id,
approvers: selectedApprovers
},
{ onSettled: () => {} }
{ onSettled: () => { } }
);
}
} else {
@@ -95,7 +101,7 @@ export const ApprovalPolicyRow = ({
);
}
}}
>
>
<DropdownMenuTrigger
asChild
disabled={
@@ -116,8 +122,8 @@ export const ApprovalPolicyRow = ({
<DropdownMenuLabel>
Select members that are allowed to approve changes
</DropdownMenuLabel>
{members?.map(({ id, user }) => {
const userId = policy.policyType === PolicyType.ChangePolicy ? user.id : id;
{members?.map(({ user }) => {
const userId = user.id;
const isChecked = selectedApprovers.includes(userId);
return (
<DropdownMenuItem
@@ -138,6 +144,76 @@ export const ApprovalPolicyRow = ({
</DropdownMenuContent>
</DropdownMenu>
</Td>
<Td>
<DropdownMenu
onOpenChange={(isOpen) => {
if (!isOpen) {
if (policy.policyType === PolicyType.AccessPolicy) {
updateAccessApprovalPolicy(
{
projectSlug,
id: policy.id,
approvers: selectedApprovers,
groupApprovers: selectedGroupApprovers
},
{ onSettled: () => { } }
);
} else {
updateSecretApprovalPolicy(
{
workspaceId,
id: policy.id,
approvers: selectedApprovers,
},
{ onSettled: () => { } }
);
}
} else {
setSelectedGroupApprovers(policy.policyType === PolicyType.ChangePolicy
? policy?.groupApprovers?.map(({ groupId }) => groupId) || []
: policy?.groupApprovers?.map(({groupId}) => groupId) || []
);
}
}}
>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={selectedGroupApprovers?.length ? `${selectedGroupApprovers.length} selected` : "None"}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>
Select groups that are allowed to approve requests
</DropdownMenuLabel>
{groups && groups.map(({ group }) => {
const { id } = group;
const isChecked = selectedGroupApprovers?.includes(id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
setSelectedGroupApprovers(
isChecked
? selectedGroupApprovers?.filter((el: string) => el !== id)
: [...(selectedGroupApprovers || []), id]
);
}}
key={`create-policy-groups-${id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{group.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</Td>
<Td>{policy.approvals}</Td>
<Td>
<Badge className={policyDetails[policy.policyType].className}>