mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 11:26:17 +00:00
Fix: Added support for request access
This commit is contained in:
@@ -4,3 +4,4 @@ frontend/src/views/Project/MembersPage/components/IdentityTab/components/Identit
|
|||||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
|
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
|
||||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
||||||
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
||||||
|
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
||||||
|
|||||||
+234
-36
@@ -1,9 +1,11 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
faArrowRotateLeft,
|
faArrowRotateLeft,
|
||||||
faCaretDown,
|
faCaretDown,
|
||||||
faCheck,
|
faCheck,
|
||||||
faClock,
|
faClock,
|
||||||
|
faLockOpen,
|
||||||
faPlus,
|
faPlus,
|
||||||
faTrash
|
faTrash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -44,11 +46,13 @@ import {
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
TProjectUserPrivilege,
|
TProjectUserPrivilege,
|
||||||
|
useCreateAccessRequest,
|
||||||
useCreateProjectUserAdditionalPrivilege,
|
useCreateProjectUserAdditionalPrivilege,
|
||||||
useDeleteProjectUserAdditionalPrivilege,
|
useDeleteProjectUserAdditionalPrivilege,
|
||||||
useListProjectUserPrivileges,
|
useListProjectUserPrivileges,
|
||||||
useUpdateProjectUserAdditionalPrivilege
|
useUpdateProjectUserAdditionalPrivilege
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
|
||||||
const secretPermissionSchema = z.object({
|
const secretPermissionSchema = z.object({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
@@ -70,51 +74,107 @@ const secretPermissionSchema = z.object({
|
|||||||
])
|
])
|
||||||
});
|
});
|
||||||
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
||||||
const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPrivilege }) => {
|
export const SpecificPrivilegeSecretForm = ({
|
||||||
|
privilege,
|
||||||
|
policies,
|
||||||
|
onClose
|
||||||
|
}: {
|
||||||
|
privilege?: TProjectUserPrivilege;
|
||||||
|
policies?: TAccessApprovalPolicy[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { membership: projectMembership } = useProjectPermission();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
"deletePrivilege"
|
"deletePrivilege",
|
||||||
|
"requestAccess"
|
||||||
] as const);
|
] as const);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled = permission.cannot(
|
const isMemberEditDisabled =
|
||||||
ProjectPermissionActions.Edit,
|
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege;
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege();
|
const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege();
|
||||||
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
||||||
|
const requestAccess = useCreateAccessRequest();
|
||||||
|
|
||||||
const privilegeForm = useForm<TSecretPermissionForm>({
|
const privilegeForm = useForm<TSecretPermissionForm>({
|
||||||
resolver: zodResolver(secretPermissionSchema),
|
resolver: zodResolver(secretPermissionSchema),
|
||||||
values: {
|
values: {
|
||||||
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
...(privilege
|
||||||
// secret path will be inside $glob operator
|
? {
|
||||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
|
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
||||||
read: privilege.permissions?.some(({ action }) =>
|
// secret path will be inside $glob operator
|
||||||
action.includes(ProjectPermissionActions.Read)
|
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
|
||||||
),
|
read: privilege.permissions?.some(({ action }) =>
|
||||||
edit: privilege.permissions?.some(({ action }) =>
|
action.includes(ProjectPermissionActions.Read)
|
||||||
action.includes(ProjectPermissionActions.Edit)
|
),
|
||||||
),
|
edit: privilege.permissions?.some(({ action }) =>
|
||||||
create: privilege.permissions?.some(({ action }) =>
|
action.includes(ProjectPermissionActions.Edit)
|
||||||
action.includes(ProjectPermissionActions.Create)
|
),
|
||||||
),
|
create: privilege.permissions?.some(({ action }) =>
|
||||||
delete: privilege.permissions?.some(({ action }) =>
|
action.includes(ProjectPermissionActions.Create)
|
||||||
action.includes(ProjectPermissionActions.Delete)
|
),
|
||||||
),
|
delete: privilege.permissions?.some(({ action }) =>
|
||||||
// zod will pick it
|
action.includes(ProjectPermissionActions.Delete)
|
||||||
temporaryAccess: privilege
|
),
|
||||||
|
// zod will pick it
|
||||||
|
temporaryAccess: privilege
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
environmentSlug: currentWorkspace?.environments?.[0].slug!,
|
||||||
|
read: false,
|
||||||
|
edit: false,
|
||||||
|
create: false,
|
||||||
|
delete: false,
|
||||||
|
temporaryAccess: {
|
||||||
|
isTemporary: false
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
||||||
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
|
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
|
||||||
|
const selectedEnvironment = privilegeForm.watch("environmentSlug");
|
||||||
|
const secretPath = privilegeForm.watch("secretPath");
|
||||||
|
|
||||||
|
const readAccess = privilegeForm.watch("read");
|
||||||
|
const createAccess = privilegeForm.watch("create");
|
||||||
|
const editAccess = privilegeForm.watch("edit");
|
||||||
|
const deleteAccess = privilegeForm.watch("delete");
|
||||||
|
|
||||||
|
const accessSelected = readAccess || createAccess || editAccess || deleteAccess;
|
||||||
|
|
||||||
|
const selectablePaths = useMemo(() => {
|
||||||
|
if (!policies) return [];
|
||||||
|
const environmentPolicies = policies.filter(
|
||||||
|
(policy) => policy.environment.slug === selectedEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
|
privilegeForm.setValue("secretPath", "", {
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...environmentPolicies.map((policy) => policy.secretPath)];
|
||||||
|
}, [policies, selectedEnvironment]);
|
||||||
|
|
||||||
const isTemporary = temporaryAccessField?.isTemporary;
|
const isTemporary = temporaryAccessField?.isTemporary;
|
||||||
const isExpired =
|
const isExpired =
|
||||||
temporaryAccessField.isTemporary &&
|
temporaryAccessField.isTemporary &&
|
||||||
new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || "");
|
new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
const handleUpdatePrivilege = async (data: TSecretPermissionForm) => {
|
const handleUpdatePrivilege = async (data: TSecretPermissionForm) => {
|
||||||
|
if (!privilege) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "No privilege to update found.",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateUserPrivilege.isLoading) return;
|
if (updateUserPrivilege.isLoading) return;
|
||||||
try {
|
try {
|
||||||
const actions = [
|
const actions = [
|
||||||
@@ -152,6 +212,15 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePrivilege = async () => {
|
const handleDeletePrivilege = async () => {
|
||||||
|
if (!privilege) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "No privilege to delete found.",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (deleteUserPrivilege.isLoading) return;
|
if (deleteUserPrivilege.isLoading) return;
|
||||||
try {
|
try {
|
||||||
await deleteUserPrivilege.mutateAsync({
|
await deleteUserPrivilege.mutateAsync({
|
||||||
@@ -170,35 +239,122 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getAccessLabel = (exactTime = false) => {
|
const getAccessLabel = (exactTime = false) => {
|
||||||
if (isExpired) return "Access expired";
|
if (isExpired) return "Access expired";
|
||||||
if (!temporaryAccessField?.isTemporary) return "Permanent";
|
if (!temporaryAccessField?.isTemporary) return "Permanent";
|
||||||
|
|
||||||
if (exactTime)
|
if (exactTime)
|
||||||
return `Until ${format(
|
return `Until ${format(
|
||||||
new Date(temporaryAccessField.temporaryAccessEndTime || ""),
|
new Date(temporaryAccessField.temporaryAccessEndTime || ""),
|
||||||
"yyyy-MM-dd HH:mm:ss"
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
)}`;
|
)}`;
|
||||||
return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date());
|
return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date());
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is used for requesting access additional privileges, not directly creating a privilege!
|
||||||
|
const handleRequestAccess = async (data: TSecretPermissionForm) => {
|
||||||
|
if (!policies) return;
|
||||||
|
if (!currentWorkspace) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "No workspace found.",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.secretPath) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Please select a secret path",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{ action: ProjectPermissionActions.Read, allowed: data.read },
|
||||||
|
{ action: ProjectPermissionActions.Create, allowed: data.create },
|
||||||
|
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
||||||
|
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
||||||
|
];
|
||||||
|
const conditions: Record<string, any> = { environment: data.environmentSlug };
|
||||||
|
if (data.secretPath) {
|
||||||
|
conditions.secretPath = { $glob: data.secretPath };
|
||||||
|
}
|
||||||
|
await requestAccess.mutateAsync({
|
||||||
|
...data,
|
||||||
|
...(data.temporaryAccess.isTemporary && {
|
||||||
|
temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime,
|
||||||
|
temporaryRange: data.temporaryAccess.temporaryRange,
|
||||||
|
temporaryMode: "relative"
|
||||||
|
}),
|
||||||
|
envSlug: data.environmentSlug,
|
||||||
|
secretPath: data.secretPath,
|
||||||
|
projectSlug: currentWorkspace.slug,
|
||||||
|
projectMembershipId: projectMembership.id,
|
||||||
|
isTemporary: data.temporaryAccess.isTemporary,
|
||||||
|
permissions: actions
|
||||||
|
.filter(({ allowed }) => allowed)
|
||||||
|
.map(({ action }) => ({
|
||||||
|
action,
|
||||||
|
subject: [ProjectPermissionSub.Secrets],
|
||||||
|
conditions
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully requested access"
|
||||||
|
});
|
||||||
|
privilegeForm.reset();
|
||||||
|
if (onClose) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data: TSecretPermissionForm) => {
|
||||||
|
if (privilege) {
|
||||||
|
handleUpdatePrivilege(data);
|
||||||
|
} else {
|
||||||
|
handleRequestAccess(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccessLabel = (exactTime = false) => {
|
||||||
|
if (isExpired) return "Access expired";
|
||||||
|
if (!temporaryAccessField?.isTemporary) return "Permanent";
|
||||||
|
|
||||||
|
if (exactTime)
|
||||||
|
return `Until ${format(
|
||||||
|
new Date(temporaryAccessField.temporaryAccessEndTime || ""),
|
||||||
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
|
)}`;
|
||||||
|
return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date());
|
||||||
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4 w-full">
|
||||||
<form onSubmit={privilegeForm.handleSubmit(handleUpdatePrivilege)}>
|
<form onSubmit={privilegeForm.handleSubmit(handleSubmit)}>
|
||||||
<div className="flex items-start space-x-4">
|
<div className={twMerge("flex items-start gap-4", !privilege && "flex-wrap")}>
|
||||||
<Controller
|
<Controller
|
||||||
control={privilegeForm.control}
|
control={privilegeForm.control}
|
||||||
name="environmentSlug"
|
name="environmentSlug"
|
||||||
render={({ field: { onChange, ...field } }) => (
|
render={({ field: { onChange, ...field } }) => (
|
||||||
<FormControl label="Env">
|
<FormControl label="Environment">
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
isDisabled={isMemberEditDisabled}
|
isDisabled={isMemberEditDisabled}
|
||||||
className="bg-mineshaft-600 hover:bg-mineshaft-500"
|
className="bg-mineshaft-600 hover:bg-mineshaft-500"
|
||||||
onValueChange={(e) => onChange(e)}
|
onValueChange={(e) => onChange(e)}
|
||||||
>
|
>
|
||||||
{currentWorkspace?.environments?.map(({ slug, id }) => (
|
{currentWorkspace?.environments?.map(({ slug, id, name }) => (
|
||||||
<SelectItem value={slug} key={id}>
|
<SelectItem value={slug} key={id}>
|
||||||
{slug}
|
{name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -208,8 +364,28 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
<Controller
|
<Controller
|
||||||
control={privilegeForm.control}
|
control={privilegeForm.control}
|
||||||
name="secretPath"
|
name="secretPath"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormControl label="Secret Path">
|
console.log(policies);
|
||||||
|
if (policies) {
|
||||||
|
return (
|
||||||
|
<FormControl label="Secret Path">
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
isDisabled={isMemberEditDisabled || !selectablePaths.length}
|
||||||
|
className="w-48"
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
>
|
||||||
|
{selectablePaths.map((path) => (
|
||||||
|
<SelectItem value={path} key={path}>
|
||||||
|
{path}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormControl label="Secret Path">
|
||||||
<SecretPathInput
|
<SecretPathInput
|
||||||
{...field}
|
{...field}
|
||||||
isDisabled={isMemberEditDisabled}
|
isDisabled={isMemberEditDisabled}
|
||||||
@@ -217,7 +393,8 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
environment={selectedEnvironmentSlug}
|
environment={selectedEnvironmentSlug}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-grow justify-between">
|
<div className="flex flex-grow justify-between">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -285,7 +462,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-7 flex items-center space-x-2">
|
<div className="mt-6 flex items-center space-x-2">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger disabled={isMemberEditDisabled}>
|
<PopoverTrigger disabled={isMemberEditDisabled}>
|
||||||
<div>
|
<div>
|
||||||
@@ -301,7 +478,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
isExpired && "text-red-600"
|
isExpired && "text-red-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getAccessLabel()}
|
{getAccessLabel(false)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -382,7 +559,8 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
{privilegeForm.formState.isDirty ? (
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{privilegeForm.formState.isDirty && privilege ? (
|
||||||
<>
|
<>
|
||||||
<Tooltip content="Cancel" className="mr-4">
|
<Tooltip content="Cancel" className="mr-4">
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -413,7 +591,8 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : // eslint-disable-next-line no-nested-ternary
|
||||||
|
privilege ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={isMemberEditDisabled ? "Access restricted" : "Delete"}
|
content={isMemberEditDisabled ? "Access restricted" : "Delete"}
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@@ -428,9 +607,28 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
<FontAwesomeIcon icon={faTrash} />
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!!policies && (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={privilegeForm.formState.isSubmitting || requestAccess.isLoading}
|
||||||
|
isDisabled={
|
||||||
|
isMemberEditDisabled ||
|
||||||
|
!policies.length ||
|
||||||
|
!privilegeForm.formState.isValid ||
|
||||||
|
!secretPath ||
|
||||||
|
!accessSelected
|
||||||
|
}
|
||||||
|
className="mt-4"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
|
||||||
|
>
|
||||||
|
Request access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deletePrivilege.isOpen}
|
isOpen={popUp.deletePrivilege.isOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user