mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 08:34:52 +00:00
feat: allow access approvals with user groups
This commit is contained in:
@@ -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;
|
||||
|
||||
+6
-1
@@ -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)}
|
||||
/>
|
||||
|
||||
+61
-8
@@ -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"
|
||||
|
||||
+82
-6
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user