Compare commits

...

12 Commits

Author SHA1 Message Date
x032205
2e256e4282 Tooltip 2025-06-26 18:14:48 -04:00
x032205
dcd21883d1 Clarify relationship between path and key schema for AWS parameter store
docs
2025-06-26 17:02:21 -04:00
Scott Wilson
205442bff5 Merge pull request #3859 from Infisical/overview-ui-improvements
improvement(secret-overview): Add collapsed environment view to secret overview page
2025-06-26 09:24:33 -07:00
Scott Wilson
e8d19eb823 improvement: disable tooltip hover content for env name tooltip 2025-06-26 09:12:11 -07:00
Scott Wilson
5d30215ea7 improvement: increase env tooltip max width and adjust alignment 2025-06-26 07:56:47 -07:00
Scott Wilson
29fedfdde5 Merge pull request #3850 from Infisical/policy-edit-revisions
improvement(project-policies): Revamp edit role page and access tree
2025-06-26 07:46:35 -07:00
Scott Wilson
b5317d1d75 fix: add ability to remove non-conditional rules 2025-06-26 07:37:30 -07:00
Scott Wilson
86c145301e improvement: add collapsed environment view to secret overview page and minor ui adjustments 2025-06-25 16:49:34 -07:00
Scott Wilson
5b4790ee78 improvements: truncate environment selection and only show visualize access when expanded 2025-06-25 09:09:08 -07:00
Scott Wilson
8683693103 improvement: address greptile feedback 2025-06-24 15:35:42 -07:00
Scott Wilson
737fffcceb improvement: address greptile feedback 2025-06-24 15:35:08 -07:00
Scott Wilson
ffac24ce75 improvement: revise edit role page and access tree 2025-06-24 15:23:27 -07:00
36 changed files with 910 additions and 1872 deletions

View File

@@ -148,3 +148,11 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
``` ```
</Tab> </Tab>
</Tabs> </Tabs>
## FAQ
<AccordionGroup>
<Accordion title="What's the relationship between 'path' and 'key schema'?">
The path is required and will be prepended to the key schema. For example, if you have a path of `/demo/path/` and a key schema of `INFISICAL_{{secretKey}}`, then the result will be `/demo/path/INFISICAL_{{secretKey}}`.
</Accordion>
</AccordionGroup>

View File

@@ -2,10 +2,9 @@ import { useCallback, useEffect, useState } from "react";
import { MongoAbility, MongoQuery } from "@casl/ability"; import { MongoAbility, MongoQuery } from "@casl/ability";
import { import {
faAnglesUp, faAnglesUp,
faArrowUpRightFromSquare,
faDownLeftAndUpRightToCenter,
faUpRightAndDownLeftFromCenter, faUpRightAndDownLeftFromCenter,
faWindowRestore faWindowRestore,
faXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -23,8 +22,8 @@ import {
} from "@xyflow/react"; } from "@xyflow/react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2"; import { Button, IconButton, Select, SelectItem, Spinner, Tooltip } from "@app/components/v2";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet, ProjectPermissionSub } from "@app/context/ProjectPermissionContext";
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput"; import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode"; import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
@@ -36,15 +35,17 @@ import { ViewMode } from "./types";
export type AccessTreeProps = { export type AccessTreeProps = {
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>; permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
subject: ProjectPermissionSub;
onClose: () => void;
}; };
const EdgeTypes = { base: BasePermissionEdge }; const EdgeTypes = { base: BasePermissionEdge };
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode }; const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
const AccessTreeContent = ({ permissions }: AccessTreeProps) => { const AccessTreeContent = ({ permissions, subject, onClose }: AccessTreeProps) => {
const [selectedPath, setSelectedPath] = useState<string>("/"); const [selectedPath, setSelectedPath] = useState<string>("/");
const accessTreeData = useAccessTree(permissions, selectedPath); const accessTreeData = useAccessTree(permissions, selectedPath, subject);
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData; const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
const [initialRender, setInitialRender] = useState(true); const [initialRender, setInitialRender] = useState(true);
@@ -78,32 +79,32 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
useEffect(() => { useEffect(() => {
setInitialRender(true); setInitialRender(true);
}, [selectedPath, environment]); }, [selectedPath, environment, subject, viewMode]);
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
if (initialRender) { if (initialRender) {
timer = setTimeout(() => { timer = setTimeout(() => {
goToRootNode(); fitView({ duration: 500 });
setInitialRender(false); setInitialRender(false);
}, 500); }, 50);
} }
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [nodes, edges, getViewport(), initialRender, goToRootNode]); }, [nodes, edges, getViewport(), initialRender, fitView]);
const handleToggleModalView = () => const handleToggleModalView = () =>
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal)); setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
const handleToggleUndockedView = () => const handleToggleView = () =>
setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked)); setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Undocked : ViewMode.Modal));
const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`; const expandButtonLabel = viewMode === ViewMode.Modal ? "Anchor View" : "Expand View";
const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`; const hideButtonLabel = "Hide Access Tree";
return ( return (
<div <div
className={twMerge( className={twMerge(
"w-full", "mt-4 w-full",
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10", viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
viewMode === ViewMode.Undocked && viewMode === ViewMode.Undocked &&
"fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]" "fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]"
@@ -130,7 +131,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
type="submit" type="submit"
className="h-10 rounded-r-none bg-mineshaft-700" className="h-10 rounded-r-none bg-mineshaft-700"
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />} leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
onClick={handleToggleUndockedView} onClick={handleToggleView}
> >
Undock Undock
</Button> </Button>
@@ -176,48 +177,62 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
<Spinner /> <Spinner />
</Panel> </Panel>
)} )}
{viewMode !== ViewMode.Undocked && (
<Panel position="top-left" className="flex gap-2">
<Select
value={environment}
onValueChange={accessTreeData.setEnvironment}
className="w-60"
position="popper"
dropdownContainerClassName="max-w-none"
aria-label="Environment"
>
{Object.values(accessTreeData.environments).map((env) => (
<SelectItem
key={env.slug}
value={env.slug}
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 truncate font-medium">{env.name}</div>
</SelectItem>
))}
</Select>
<AccessTreeSecretPathInput
placeholder="Provide a path, default is /"
environment={environment}
value={selectedPath}
onChange={setSelectedPath}
/>
</Panel>
)}
{viewMode !== ViewMode.Docked && ( {viewMode !== ViewMode.Docked && (
<Panel position="top-right" className="flex gap-1.5"> <Panel position="top-right" className="flex gap-2">
{viewMode !== ViewMode.Undocked && ( <Tooltip position="bottom" align="center" content={expandButtonLabel}>
<AccessTreeSecretPathInput
placeholder="Provide a path, default is /"
environment={environment}
value={selectedPath}
onChange={setSelectedPath}
/>
)}
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
<IconButton <IconButton
className="ml-1 w-10 rounded" className="rounded p-2"
colorSchema="secondary" colorSchema="secondary"
variant="plain" variant="plain"
onClick={handleToggleUndockedView} onClick={handleToggleView}
ariaLabel={undockButtonLabel} ariaLabel={expandButtonLabel}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={ icon={
viewMode === ViewMode.Undocked viewMode === ViewMode.Undocked
? faArrowUpRightFromSquare ? faUpRightAndDownLeftFromCenter
: faWindowRestore : faWindowRestore
} }
/> />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip align="end" position="bottom" content={windowButtonLabel}> <Tooltip align="end" position="bottom" content={hideButtonLabel}>
<IconButton <IconButton
className="w-10 rounded" className="rounded p-2"
colorSchema="secondary" colorSchema="secondary"
variant="plain" variant="plain"
onClick={handleToggleModalView} onClick={onClose}
ariaLabel={windowButtonLabel} ariaLabel={hideButtonLabel}
> >
<FontAwesomeIcon <FontAwesomeIcon icon={faXmark} />
icon={
viewMode === ViewMode.Modal
? faDownLeftAndUpRightToCenter
: faUpRightAndDownLeftFromCenter
}
/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Panel> </Panel>
@@ -253,6 +268,9 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
}; };
export const AccessTree = (props: AccessTreeProps) => { export const AccessTree = (props: AccessTreeProps) => {
const { subject } = props;
if (!subject) return null;
return ( return (
<AccessTreeErrorBoundary {...props}> <AccessTreeErrorBoundary {...props}>
<AccessTreeProvider> <AccessTreeProvider>

View File

@@ -29,7 +29,7 @@ export type AccessTreeForm = { metadata: { key: string; value: string }[] };
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => { export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
const [secretName, setSecretName] = useState(""); const [secretName, setSecretName] = useState("");
const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } }); const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } });
const [viewMode, setViewMode] = useState(ViewMode.Docked); const [viewMode, setViewMode] = useState(ViewMode.Modal);
const value = useMemo( const value = useMemo(
() => ({ () => ({

View File

@@ -33,7 +33,8 @@ type LevelFolderMap = Record<
export const useAccessTree = ( export const useAccessTree = (
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>, permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
searchPath: string searchPath: string,
subject: ProjectPermissionSub
) => { ) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext(); const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
@@ -41,7 +42,6 @@ export const useAccessTree = (
const metadata = useWatch({ control, name: "metadata" }); const metadata = useWatch({ control, name: "metadata" });
const [nodes, setNodes] = useNodesState<Node>([]); const [nodes, setNodes] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]); const [edges, setEdges] = useEdgesState<Edge>([]);
const [subject, setSubject] = useState(ProjectPermissionSub.Secrets);
const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? ""); const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? "");
const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders( const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders(
currentWorkspace.id currentWorkspace.id
@@ -147,9 +147,7 @@ export const useAccessTree = (
const roleNode = createRoleNode({ const roleNode = createRoleNode({
subject, subject,
environment: slug, environment: slug,
environments: environmentsFolders, environments: environmentsFolders
onSubjectChange: setSubject,
onEnvironmentChange: setEnvironment
}); });
const actionRuleMap = getSubjectActionRuleMap(subject, permissions); const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
@@ -280,7 +278,6 @@ export const useAccessTree = (
subject, subject,
environment, environment,
setEnvironment, setEnvironment,
setSubject,
isLoading: isPending, isLoading: isPending,
environments: currentWorkspace.environments, environments: currentWorkspace.environments,
secretName, secretName,

View File

@@ -81,7 +81,7 @@ export const AccessTreeSecretPathInput = ({
<FontAwesomeIcon icon={faSearch} /> <FontAwesomeIcon icon={faSearch} />
</div> </div>
) : ( ) : (
<Tooltip position="bottom" content="Search paths"> <Tooltip position="bottom" content="Search Paths">
<div <div
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white" className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
onClick={toggleSearch} onClick={toggleSearch}

View File

@@ -3,7 +3,6 @@ import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Handle, NodeProps, Position } from "@xyflow/react"; import { Handle, NodeProps, Position } from "@xyflow/react";
import { Select, SelectItem } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
@@ -29,7 +28,7 @@ const formatLabel = (text: string) => {
}; };
export const RoleNode = ({ export const RoleNode = ({
data: { subject, environment, onSubjectChange, onEnvironmentChange, environments } data: { subject }
}: NodeProps & { }: NodeProps & {
data: ReturnType<typeof createRoleNode>["data"] & { data: ReturnType<typeof createRoleNode>["data"] & {
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>; onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
@@ -44,61 +43,10 @@ export const RoleNode = ({
className="pointer-events-none !cursor-pointer opacity-0" className="pointer-events-none !cursor-pointer opacity-0"
position={Position.Top} position={Position.Top}
/> />
<div className="flex w-full flex-col items-center justify-center rounded-md border-2 border-mineshaft-500 bg-gradient-to-b from-mineshaft-700 to-mineshaft-800 px-5 py-4 font-inter shadow-2xl"> <div className="flex h-14 w-full flex-col items-center justify-center rounded-md border border-mineshaft bg-mineshaft-800 px-2 py-3 font-inter shadow-lg transition-opacity duration-500">
<div className="flex w-full min-w-[240px] flex-col gap-4"> <div className="flex items-center space-x-2 text-mineshaft-100">
<div className="flex w-full flex-col gap-1.5"> {getSubjectIcon(subject)}
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Subject</div> <span className="text-sm">{formatLabel(subject)} Access</span>
<Select
value={subject}
onValueChange={(value) => onSubjectChange(value as ProjectPermissionSub)}
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
position="popper"
dropdownContainerClassName="max-w-none"
aria-label="Subject"
>
{[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretImports
].map((sub) => {
return (
<SelectItem
className="relative flex items-center gap-2 py-2 pl-8 pr-8 text-sm capitalize hover:bg-mineshaft-700"
value={sub}
key={sub}
>
<div className="flex items-center gap-3">
{getSubjectIcon(sub)}
<span className="font-medium">{formatLabel(sub)}</span>
</div>
</SelectItem>
);
})}
</Select>
</div>
<div className="flex w-full flex-col gap-1.5">
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Environment</div>
<Select
value={environment}
onValueChange={onEnvironmentChange}
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
position="popper"
dropdownContainerClassName="max-w-none"
aria-label="Environment"
>
{Object.values(environments).map((env) => (
<SelectItem
key={env.slug}
value={env.slug}
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">{env.name}</div>
</SelectItem>
))}
</Select>
</div>
</div> </div>
</div> </div>
<Handle <Handle

View File

@@ -1,5 +1,3 @@
import { Dispatch, SetStateAction } from "react";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
@@ -8,24 +6,18 @@ import { PermissionNode } from "../types";
export const createRoleNode = ({ export const createRoleNode = ({
subject, subject,
environment, environment,
environments, environments
onSubjectChange,
onEnvironmentChange
}: { }: {
subject: string; subject: ProjectPermissionSub;
environment: string; environment: string;
environments: TProjectEnvironmentsFolders; environments: TProjectEnvironmentsFolders;
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
onEnvironmentChange: (value: string) => void;
}) => ({ }) => ({
id: `role-${subject}-${environment}`, id: `role-${subject}-${environment}`,
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
data: { data: {
subject, subject,
environment, environment,
environments, environments
onSubjectChange,
onEnvironmentChange
}, },
type: PermissionNode.Role, type: PermissionNode.Role,
height: 48, height: 48,

View File

@@ -39,16 +39,6 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
const positionedNodes = nodes.map((node) => { const positionedNodes = nodes.map((node) => {
const { x, y } = dagre.node(node.id); const { x, y } = dagre.node(node.id);
if (node.type === "role") {
return {
...node,
position: {
x: x - (node.width ? node.width / 2 : 0),
y: y - 150
}
};
}
return { return {
...node, ...node,
position: { position: {

View File

@@ -173,17 +173,19 @@ export const ProjectTemplateEditRoleForm = ({
<div className="p-4"> <div className="p-4">
<div className="mb-2 text-lg">Policies</div> <div className="mb-2 text-lg">Policies</div>
<PermissionEmptyState /> <PermissionEmptyState />
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( <div>
<GeneralPermissionPolicies {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
subject={subject} <GeneralPermissionPolicies
actions={PROJECT_PERMISSION_OBJECT[subject].actions} subject={subject}
title={PROJECT_PERMISSION_OBJECT[subject].title} actions={PROJECT_PERMISSION_OBJECT[subject].actions}
key={`project-permission-${subject}`} title={PROJECT_PERMISSION_OBJECT[subject].title}
isDisabled={isDisabled} key={`project-permission-${subject}`}
> isDisabled={isDisabled}
{renderConditionalComponents(subject, isDisabled)} >
</GeneralPermissionPolicies> {renderConditionalComponents(subject, isDisabled)}
))} </GeneralPermissionPolicies>
))}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>

View File

@@ -30,7 +30,29 @@ export const AwsParameterStoreSyncFields = () => {
/> />
<Controller <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Path"> <FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Path"
tooltipText={
<>
The path is required and will be prepended to the key schema. For example, if you
have a path of{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
/demo/path/
</code>{" "}
and a key schema of{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
INFISICAL_{"{{secretKey}}"}
</code>
, then the result will be{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
/demo/path/INFISICAL_{"{{secretKey}}"}
</code>
</>
}
tooltipClassName="max-w-lg"
>
<Input value={value} onChange={onChange} placeholder="Path..." /> <Input value={value} onChange={onChange} placeholder="Path..." />
</FormControl> </FormControl>
)} )}

View File

@@ -40,9 +40,9 @@ export const Checkbox = ({
<div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}> <div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
className={twMerge( className={twMerge(
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500", "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400/50 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
isDisabled && "bg-bunker-400 hover:bg-bunker-400", isDisabled && "bg-bunker-400 hover:bg-bunker-400",
isChecked && "bg-primary hover:bg-primary", isChecked && "border-primary/30 bg-primary/10",
Boolean(children) && "mr-3", Boolean(children) && "mr-3",
className className
)} )}
@@ -53,7 +53,10 @@ export const Checkbox = ({
id={id} id={id}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
className={twMerge(`${checkIndicatorBg || "text-bunker-800"}`, indicatorClassName)} className={twMerge(
`${checkIndicatorBg || "mt-[0.1rem] text-mineshaft-200"}`,
indicatorClassName
)}
> >
{isIndeterminate ? ( {isIndeterminate ? (
<FontAwesomeIcon icon={faMinus} size="sm" /> <FontAwesomeIcon icon={faMinus} size="sm" />

View File

@@ -1,5 +1,6 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { TooltipProps as RootProps } from "@radix-ui/react-tooltip";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & { export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
@@ -14,6 +15,7 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
isDisabled?: boolean; isDisabled?: boolean;
center?: boolean; center?: boolean;
size?: "sm" | "md"; size?: "sm" | "md";
rootProps?: RootProps;
}; };
export const Tooltip = ({ export const Tooltip = ({
@@ -28,12 +30,14 @@ export const Tooltip = ({
isDisabled, isDisabled,
position = "top", position = "top",
size = "md", size = "md",
rootProps,
...props ...props
}: TooltipProps) => }: TooltipProps) =>
// just render children if tooltip content is empty // just render children if tooltip content is empty
content ? ( content ? (
<TooltipPrimitive.Root <TooltipPrimitive.Root
delayDuration={50} delayDuration={50}
{...rootProps}
open={isOpen} open={isOpen}
defaultOpen={defaultOpen} defaultOpen={defaultOpen}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}

View File

@@ -161,6 +161,18 @@ export type IdentityManagementSubjectFields = {
identityId: string; identityId: string;
}; };
export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.Secrets
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.Identity
| ProjectPermissionSub.SshHosts
| ProjectPermissionSub.PkiSubscribers
| ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation;
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to", [PermissionConditionOperators.$EQ]: "equal to",
[PermissionConditionOperators.$IN]: "in", [PermissionConditionOperators.$IN]: "in",

View File

@@ -352,19 +352,21 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
<div className="p-4"> <div className="p-4">
<div className="mb-2 text-lg">Policies</div> <div className="mb-2 text-lg">Policies</div>
{(isCreate || !isPending) && <PermissionEmptyState />} {(isCreate || !isPending) && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map( <div>
(permissionSubject) => ( {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
<GeneralPermissionPolicies (permissionSubject) => (
subject={permissionSubject} <GeneralPermissionPolicies
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions} subject={permissionSubject}
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title} actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
key={`project-permission-${permissionSubject}`} title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
isDisabled={isDisabled} key={`project-permission-${permissionSubject}`}
> isDisabled={isDisabled}
{renderConditionalComponents(permissionSubject, isDisabled)} >
</GeneralPermissionPolicies> {renderConditionalComponents(permissionSubject, isDisabled)}
) </GeneralPermissionPolicies>
)} )
)}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>

View File

@@ -348,17 +348,19 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
<div className="p-4"> <div className="p-4">
<div className="mb-2 text-lg">Policies</div> <div className="mb-2 text-lg">Policies</div>
{(isCreate || !isPending) && <PermissionEmptyState />} {(isCreate || !isPending) && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( <div>
<GeneralPermissionPolicies {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
subject={subject} <GeneralPermissionPolicies
actions={PROJECT_PERMISSION_OBJECT[subject].actions} subject={subject}
title={PROJECT_PERMISSION_OBJECT[subject].title} actions={PROJECT_PERMISSION_OBJECT[subject].actions}
key={`project-permission-${subject}`} title={PROJECT_PERMISSION_OBJECT[subject].title}
isDisabled={isDisabled} key={`project-permission-${subject}`}
> isDisabled={isDisabled}
{renderConditionalComponents(subject, isDisabled)} >
</GeneralPermissionPolicies> {renderConditionalComponents(subject, isDisabled)}
))} </GeneralPermissionPolicies>
))}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>

View File

@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faCopy, faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -12,19 +14,17 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader, PageHeader
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
import { RolePermissionsSection } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
import { ProjectAccessControlTabs } from "@app/types/project"; import { ProjectAccessControlTabs } from "@app/types/project";
import { RoleDetailsSection } from "./components/RoleDetailsSection";
import { RoleModal } from "./components/RoleModal"; import { RoleModal } from "./components/RoleModal";
import { RolePermissionsSection } from "./components/RolePermissionsSection";
const Page = () => { const Page = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -88,17 +88,29 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}> <PageHeader
title={
<div className="flex flex-col">
<div>
<span>{data.name}</span>
<p className="text-sm font-[400] normal-case leading-3 text-mineshaft-400">
{data.slug} {data.description && `- ${data.description}`}
</p>
</div>
</div>
}
>
{isCustomRole && ( {isCustomRole && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <Button
<Tooltip content="More options"> colorSchema="secondary"
<Button variant="outline_bg">More</Button> rightIcon={<FontAwesomeIcon icon={faEllipsisV} className="ml-2" />}
</Tooltip> >
</div> Options
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-1"> <DropdownMenuContent align="end" sideOffset={2} className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role} a={ProjectPermissionSub.Role}
@@ -113,6 +125,7 @@ const Page = () => {
roleSlug roleSlug
}) })
} }
icon={<FontAwesomeIcon icon={faEdit} />}
disabled={!isAllowed} disabled={!isAllowed}
> >
Edit Role Edit Role
@@ -128,6 +141,7 @@ const Page = () => {
className={twMerge( className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)} )}
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={() => { onClick={() => {
handlePopUpOpen("duplicateRole"); handlePopUpOpen("duplicateRole");
}} }}
@@ -143,13 +157,9 @@ const Page = () => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( icon={<FontAwesomeIcon icon={faTrash} />}
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => handlePopUpOpen("deleteRole")} onClick={() => handlePopUpOpen("deleteRole")}
disabled={!isAllowed} isDisabled={!isAllowed}
> >
Delete Role Delete Role
</DropdownMenuItem> </DropdownMenuItem>
@@ -159,12 +169,7 @@ const Page = () => {
</DropdownMenu> </DropdownMenu>
)} )}
</PageHeader> </PageHeader>
<div className="flex"> <RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
<div className="mr-4 w-96">
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
</div>
<RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
</div>
</div> </div>
)} )}
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />

View File

@@ -24,7 +24,7 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
] as const); ] as const);
return ( return (
<> <div>
<Button <Button
className="h-10 rounded-r-none" className="h-10 rounded-r-none"
variant="outline_bg" variant="outline_bg"
@@ -73,6 +73,6 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
isOpen={popUp.applyTemplate.isOpen} isOpen={popUp.applyTemplate.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)}
/> />
</> </div>
); );
}; };

View File

@@ -0,0 +1,206 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
ConditionalProjectPermissionSubject,
PermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
export const ConditionsFields = ({
isDisabled,
subject,
position,
selectOptions
}: {
isDisabled: boolean | undefined;
subject: ConditionalProjectPermissionSubject;
position: number;
selectOptions: [{ value: string; label: string }, ...{ value: string; label: string }[]];
}) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${subject}.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.[subject]?.[position]?.conditions?.message ||
errors?.permissions?.[subject]?.[position]?.conditions?.root?.message;
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<div className="flex w-full items-center justify-between">
<div className="mt-2.5 flex items-center text-gray-300">
<span>Conditions</span>
<Tooltip
className="max-w-sm"
content={
<>
<p>
Conditions determine when a policy will be applied (always if no conditions are
present).
</p>
<p className="mt-3">
All conditions must evaluate to true for the policy to take effect.
</p>
</>
}
>
<FontAwesomeIcon size="xs" className="ml-1 text-mineshaft-400" icon={faInfoCircle} />
</Tooltip>
</div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg"
size="xs"
className="mt-2"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: selectOptions[0].value,
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
<div className="mt-2 flex flex-col space-y-2">
{Boolean(items.fields.length) &&
items.fields.map((el, index) => {
const condition =
(watch(`permissions.${subject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex items-start gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.${subject}.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
position="popper"
className="w-full"
>
{selectOptions.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-44 items-center space-x-2">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
position="popper"
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-bunker-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="remove"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
</div>
);
};

View File

@@ -1,26 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -28,162 +8,17 @@ type Props = {
}; };
export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.message ||
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.root
?.message;
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.DynamicSecrets}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" },
</p> { value: "metadataKey", label: "Metadata Key" },
<div className="mt-2 flex flex-col space-y-2"> { value: "metadataValue", label: "Metadata Value" }
{items.fields.map((el, index) => { ]}
const condition = watch( />
`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}`
) as {
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
<SelectItem value="metadataKey">Metadata Key</SelectItem>
<SelectItem value="metadataValue">Metadata Value</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,180 +1,26 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
isDisabled?: boolean; isDisabled?: boolean;
type: type:
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation; | ProjectPermissionSub.SecretRotation;
}; };
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => { export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${type}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={type}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" }
</p> ]}
<div className="mt-2 flex flex-col space-y-2"> />
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${type}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[type]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[type]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { Control, Controller, useFieldArray, useFormContext, useWatch } from "re
import { import {
faChevronDown, faChevronDown,
faChevronRight, faChevronRight,
faDiagramProject,
faGripVertical, faGripVertical,
faInfoCircle, faInfoCircle,
faPlus, faPlus,
@@ -27,6 +28,7 @@ type Props<T extends ProjectPermissionSub> = {
actions: TProjectPermissionObject[T]["actions"]; actions: TProjectPermissionObject[T]["actions"];
children?: JSX.Element; children?: JSX.Element;
isDisabled?: boolean; isDisabled?: boolean;
onShowAccessTree?: (subject: ProjectPermissionSub) => void;
}; };
type ActionProps = { type ActionProps = {
@@ -71,7 +73,8 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
actions, actions,
children, children,
title, title,
isDisabled isDisabled,
onShowAccessTree
}: Props<T>) => { }: Props<T>) => {
const { control, watch } = useFormContext<TFormSchema>(); const { control, watch } = useFormContext<TFormSchema>();
const { fields, remove, insert, move } = useFieldArray({ const { fields, remove, insert, move } = useFieldArray({
@@ -89,7 +92,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
const [draggedItem, setDraggedItem] = useState<number | null>(null); const [draggedItem, setDraggedItem] = useState<number | null>(null);
const [dragOverItem, setDragOverItem] = useState<number | null>(null); const [dragOverItem, setDragOverItem] = useState<number | null>(null);
if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return <div />; if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return null;
const handleDragStart = (_: React.DragEvent, index: number) => { const handleDragStart = (_: React.DragEvent, index: number) => {
setDraggedItem(index); setDraggedItem(index);
@@ -121,9 +124,9 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
}; };
return ( return (
<div className="border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"> <div className="overflow-clip border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
<div <div
className="flex cursor-pointer items-center space-x-8 px-5 py-4 text-sm text-gray-300" className="flex h-14 cursor-pointer items-center px-5 py-4 text-sm text-gray-300"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setIsOpen.toggle()} onClick={() => setIsOpen.toggle()}
@@ -133,20 +136,50 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
} }
}} }}
> >
<div> <FontAwesomeIcon className="mr-8" icon={isOpen ? faChevronDown : faChevronRight} />
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronRight} />
</div> <div className="flex-grow text-base">{title}</div>
<div className="flex-grow">{title}</div>
{fields.length > 1 && ( {fields.length > 1 && (
<div> <div>
<Tag size="xs" className="px-2"> <Tag size="xs" className="mr-2 px-2">
{fields.length} rules {fields.length} Rules
</Tag> </Tag>
</div> </div>
)} )}
{isOpen && onShowAccessTree && (
<Button
leftIcon={<FontAwesomeIcon icon={faDiagramProject} />}
variant="outline_bg"
size="xs"
className="ml-2"
onClick={(e) => {
e.stopPropagation();
onShowAccessTree(subject);
}}
>
Visualize Access
</Button>
)}
{!isDisabled && isOpen && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg"
className="ml-2"
size="xs"
onClick={(e) => {
e.stopPropagation();
insert(fields.length, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add Rule
</Button>
)}
</div> </div>
{isOpen && ( {isOpen && (
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6"> <div key={`select-${subject}-type`} className="flex flex-col space-y-3 bg-bunker-700 p-3">
{fields.map((el, rootIndex) => { {fields.map((el, rootIndex) => {
let isFullReadAccessEnabled = false; let isFullReadAccessEnabled = false;
@@ -154,78 +187,103 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any); isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
} }
const isInverted = watch(`permissions.${subject}.${rootIndex}.inverted` as any);
return ( return (
<div <div
key={el.id} key={el.id}
className={twMerge( className={twMerge(
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md", "relative rounded-md border-l-[6px] bg-mineshaft-800 px-5 py-4 transition-colors duration-300",
dragOverItem === rootIndex ? "border-2 border-blue-400" : "", isInverted ? "border-l-red-600/50" : "border-l-green-600/50",
dragOverItem === rootIndex ? "border-2 border-primary/50" : "",
draggedItem === rootIndex ? "opacity-50" : "" draggedItem === rootIndex ? "opacity-50" : ""
)} )}
onDragOver={(e) => handleDragOver(e, rootIndex)} onDragOver={(e) => handleDragOver(e, rootIndex)}
onDrop={handleDrop} onDrop={handleDrop}
> >
{!isDisabled && ( {isConditionalSubjects(subject) && (
<Tooltip position="left" content="Drag to reorder permission"> <div className="mb-4 flex items-center gap-3">
<div
draggable
onDragStart={(e) => handleDragStart(e, rootIndex)}
onDragEnd={handleDragEnd}
className="absolute right-3 top-2 cursor-move rounded-md bg-mineshaft-700 p-2 text-gray-400 hover:text-gray-200"
>
<FontAwesomeIcon icon={faGripVertical} />
</div>
</Tooltip>
)}
<div className="mb-4 flex items-center">
{isConditionalSubjects(subject) && (
<div className="flex w-full items-center text-gray-300"> <div className="flex w-full items-center text-gray-300">
<div className="w-1/4">Permission</div> <div className="mr-3">Permission</div>
<div className="mr-4 w-1/4"> <Controller
<Controller defaultValue={false as any}
defaultValue={false as any} name={`permissions.${subject}.${rootIndex}.inverted`}
name={`permissions.${subject}.${rootIndex}.inverted`} render={({ field }) => (
render={({ field }) => ( <Select
<Select value={String(field.value)}
value={String(field.value)} onValueChange={(val) => field.onChange(val === "true")}
onValueChange={(val) => field.onChange(val === "true")} containerClassName="w-40"
containerClassName="w-full" className="w-full"
className="w-full" isDisabled={isDisabled}
isDisabled={isDisabled} position="popper"
> >
<SelectItem value="false">Allow</SelectItem> <SelectItem value="false">Allow</SelectItem>
<SelectItem value="true">Forbid</SelectItem> <SelectItem value="true">Forbid</SelectItem>
</Select> </Select>
)} )}
/>
<Tooltip
asChild
content={
<>
<p>
Whether to allow or forbid the selected actions when the following
conditions (if any) are met.
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
>
<FontAwesomeIcon
icon={faInfoCircle}
size="sm"
className="ml-2 text-bunker-400"
/> />
</div> </Tooltip>
<div> {!isDisabled && (
<Tooltip <Button
asChild leftIcon={<FontAwesomeIcon icon={faTrash} />}
content={ variant="outline_bg"
<> size="xs"
<p> className="ml-auto mr-3"
Whether to allow or forbid the selected actions when the following onClick={() => remove(rootIndex)}
conditions (if any) are met. isDisabled={isDisabled}
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
> >
<FontAwesomeIcon Remove Rule
icon={faInfoCircle} </Button>
size="sm" )}
className="text-gray-400" {!isDisabled && (
/> <Tooltip position="left" content="Drag to reorder permission">
<div
draggable
onDragStart={(e) => handleDragStart(e, rootIndex)}
onDragEnd={handleDragEnd}
className="cursor-move text-bunker-300 hover:text-bunker-200"
>
<FontAwesomeIcon icon={faGripVertical} />
</div>
</Tooltip> </Tooltip>
</div> )}
</div> </div>
)} </div>
</div> )}
<div className="flex gap-4 text-gray-300"> <div className="flex flex-col text-gray-300">
<div className="w-1/4">Actions</div> <div className="flex w-full justify-between">
<div className="flex flex-grow flex-wrap justify-start gap-8"> <div className="mb-2">Actions</div>
{!isDisabled && !isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="ml-auto"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
Remove Rule
</Button>
)}
</div>
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{actions.map(({ label, value }, index) => { {actions.map(({ label, value }, index) => {
if (typeof value !== "string") return undefined; if (typeof value !== "string") return undefined;
@@ -255,41 +313,6 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
cloneElement(children, { cloneElement(children, {
position: rootIndex position: rootIndex
})} })}
<div
className={twMerge(
"mt-4 flex justify-start space-x-4",
isConditionalSubjects(subject) && "justify-end"
)}
>
{!isDisabled && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-2"
onClick={() => {
insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add policy
</Button>
)}
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="mt-2 hover:border-red"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
Remove policy
</Button>
)}{" "}
</div>
</div> </div>
); );
})} })}

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,150 +8,12 @@ type Props = {
}; };
export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.Identity;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.Identity}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "identityId", label: "Identity ID" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="identityId">Identity ID</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "identityId",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -17,17 +17,36 @@ export const getConditionOperatorHelperInfo = (type: PermissionConditionOperator
} }
}; };
// scott: we may need to pass the subject in the future to further refine returned items
export const renderOperatorSelectItems = (type: string) => { export const renderOperatorSelectItems = (type: string) => {
if (type === "secretTags") { switch (type) {
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>; case "secretTags":
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
case "identityId":
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
case "hostname":
case "name":
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
default:
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
} }
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
}; };

View File

@@ -12,7 +12,7 @@ export const PermissionEmptyState = () => {
([key, value]) => key && value?.length > 0 ([key, value]) => key && value?.length > 0
); );
if (isNotEmptyPermissions) return <div />; if (isNotEmptyPermissions) return null;
return <EmptyState title="No policies applied" className="py-8" />; return <EmptyState title="No policies applied" className="py-8" />;
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,149 +8,12 @@ type Props = {
}; };
export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.PkiSubscribers;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.PkiSubscribers}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "name", label: "Name" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="name">Name</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "name",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,149 +8,12 @@ type Props = {
}; };
export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: Props) => { export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.CertificateTemplates;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.CertificateTemplates}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "name", label: "Name" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="name">Name</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="delete"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "name",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,98 +0,0 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetProjectRoleBySlug } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleSlug: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: object) => void;
};
export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { currentWorkspace } = useWorkspace();
const { data } = useGetProjectRoleBySlug(currentWorkspace?.id ?? "", roleSlug as string);
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
(data?.slug ?? "") as ProjectMembershipRole
);
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Project Role Details</h3>
{isCustomRole && (
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Role}>
{(isAllowed) => {
return (
<Tooltip content="Edit Role">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("role", {
roleSlug
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</ProjectPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{data.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(data.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<p className="text-sm text-mineshaft-300">{data.slug}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">
{data.description?.length ? data.description : "-"}
</p>
</div>
</div>
</div>
) : (
<div />
);
};

View File

@@ -1,4 +1,4 @@
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
import { faSave } from "@fortawesome/free-solid-svg-icons"; import { faSave } from "@fortawesome/free-solid-svg-icons";
@@ -88,6 +88,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
roleSlug as string roleSlug as string
); );
const [showAccessTree, setShowAccessTree] = useState<ProjectPermissionSub | null>(null);
const form = useForm<TFormSchema>({ const form = useForm<TFormSchema>({
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
resolver: zodResolver(projectRoleFormSchema) resolver: zodResolver(projectRoleFormSchema)
@@ -133,71 +135,90 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
[JSON.stringify(permissions)] [JSON.stringify(permissions)]
); );
const isSecretManagerProject = currentWorkspace.type === ProjectType.SecretManager;
return ( return (
<div className="w-full"> <div className="w-full">
{currentWorkspace.type === ProjectType.SecretManager && (
<AccessTree permissions={formattedPermissions} />
)}
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" className="flex h-full w-full flex-1 flex-col rounded-lg border border-mineshaft-600 bg-mineshaft-900 py-4"
> >
<FormProvider {...form}> <FormProvider {...form}>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="mx-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3> <div>
<div className="flex items-center space-x-4"> <h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
{isCustomRole && ( <p className="text-sm leading-3 text-mineshaft-400">
<> Configure granular access policies
{isDirty && ( </p>
<Button
className="mr-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting}
isLoading={isSubmitting}
onClick={() => reset()}
>
Discard
</Button>
)}
<div className="flex items-center">
<Button
variant="outline_bg"
type="submit"
className={twMerge(
"mr-4 h-10 border",
isDirty && "bg-primary text-black hover:bg-primary hover:opacity-80"
)}
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} />
</div>
</>
)}
</div> </div>
</div> {isCustomRole && (
<div className="py-4"> <div className="flex items-center gap-2">
{!isPending && <PermissionEmptyState />} {isDirty && (
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]) <Button
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject)) className="mr-4 text-mineshaft-300"
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject]) variant="link"
.map((subject) => ( isDisabled={isSubmitting}
<GeneralPermissionPolicies isLoading={isSubmitting}
subject={subject} onClick={() => reset()}
actions={PROJECT_PERMISSION_OBJECT[subject].actions} >
title={PROJECT_PERMISSION_OBJECT[subject].title} Discard
key={`project-permission-${subject}`} </Button>
isDisabled={isDisabled} )}
<Button
colorSchema="secondary"
type="submit"
className={twMerge("h-10 border")}
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
{renderConditionalComponents(subject, isDisabled)} Save
</GeneralPermissionPolicies> </Button>
))} <div className="ml-2 border-l border-mineshaft-500 pl-4">
<AddPoliciesButton isDisabled={isDisabled} />
</div>
</div>
)}
</div>
<div className="flex flex-1 flex-col overflow-hidden pl-4 pr-1">
<div className="thin-scrollbar flex-1 overflow-y-scroll py-4">
{!isPending && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject))
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
.map((subject) => (
<GeneralPermissionPolicies
subject={subject}
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
title={PROJECT_PERMISSION_OBJECT[subject].title}
key={`project-permission-${subject}`}
isDisabled={isDisabled}
onShowAccessTree={
isSecretManagerProject &&
[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretImports
].includes(subject)
? setShowAccessTree
: undefined
}
>
{renderConditionalComponents(subject, isDisabled)}
</GeneralPermissionPolicies>
))}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>
{isSecretManagerProject && showAccessTree && (
<AccessTree
permissions={formattedPermissions}
subject={showAccessTree}
onClose={() => setShowAccessTree(null)}
/>
)}
</div> </div>
); );
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,159 +8,17 @@ type Props = {
}; };
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.secrets.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.secrets?.[position]?.conditions?.message ||
errors?.permissions?.secrets?.[position]?.conditions?.root?.message;
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.Secrets}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" },
</p> { value: "secretName", label: "Secret Name" },
<div className="mt-2 flex flex-col space-y-2"> { value: "secretTags", label: "Secret Tags" }
{items.fields.map((el, index) => { ]}
const condition = watch(`permissions.secrets.${position}.conditions.${index}`) as { />
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.secrets.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
<SelectItem value="secretName">Secret Name</SelectItem>
<SelectItem value="secretTags">Secret Tags</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,26 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -28,159 +8,15 @@ type Props = {
}; };
export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.message ||
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.root?.message;
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.SecretSyncs}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" }
</p> ]}
<div className="mt-2 flex flex-col space-y-2"> />
{items.fields.map((el, index) => {
const condition = watch(
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}`
) as {
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="remove"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,149 +8,12 @@ type Props = {
}; };
export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.SshHosts;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.SshHosts}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "hostname", label: "Hostname" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="hostname">Hostname</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} onChange={(e) => field.onChange(e.target.value.trim())} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "hostname",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -6,6 +6,9 @@ import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
faArrowDown, faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRightToBracket,
faArrowUp, faArrowUp,
faFileImport, faFileImport,
faFingerprint, faFingerprint,
@@ -69,7 +72,7 @@ import {
PreferenceKey, PreferenceKey,
setUserTablePreference setUserTablePreference
} from "@app/helpers/userTablePreferences"; } from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useDebounce, usePagination, usePopUp, useResetPageHelper, useToggle } from "@app/hooks";
import { import {
useCreateFolder, useCreateFolder,
useCreateSecretV3, useCreateSecretV3,
@@ -164,6 +167,18 @@ export const OverviewPage = () => {
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter); const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
const secretPath = (routerSearch?.secretPath as string) || "/"; const secretPath = (routerSearch?.secretPath as string) || "/";
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const [collapseEnvironments, setCollapseEnvironments] = useToggle(
Boolean(localStorage.getItem("overview-collapse-environments"))
);
const handleToggleNarrowHeader = () => {
setCollapseEnvironments.toggle();
if (collapseEnvironments) {
localStorage.removeItem("overview-collapse-environments");
} else {
localStorage.setItem("overview-collapse-environments", "true");
}
};
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE); const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
const [filterHistory, setFilterHistory] = useState< const [filterHistory, setFilterHistory] = useState<
@@ -1173,47 +1188,81 @@ export const OverviewPage = () => {
className="thin-scrollbar rounded-b-none" className="thin-scrollbar rounded-b-none"
> >
<Table> <Table>
<THead> <THead className={collapseEnvironments ? "h-24" : ""}>
<Tr className="sticky top-0 z-20 border-0"> <Tr
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0"> className={twMerge("sticky top-0 z-20 border-0", collapseEnvironments && "h-24")}
<div className="flex items-center border-b border-r border-mineshaft-600 pb-3 pl-3 pr-5 pt-3.5"> >
<Tooltip <Th
className="max-w-[20rem] whitespace-nowrap capitalize" className={twMerge(
content={ "sticky left-0 z-20 min-w-[20rem] border-b-0 p-0",
totalCount > 0 collapseEnvironments && "h-24"
? `${ )}
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect" >
} all folders and secrets on page` <div
: "" className={twMerge(
} "flex h-full border-b border-mineshaft-600 pb-3 pl-3 pr-5",
!collapseEnvironments && "border-r pt-3.5"
)}
>
<div
className={twMerge("flex items-center", collapseEnvironments && "mt-auto")}
> >
<div className="ml-2 mr-4"> <Tooltip
<Checkbox className="max-w-[20rem] whitespace-nowrap capitalize"
isDisabled={totalCount === 0} content={
id="checkbox-select-all-rows" totalCount > 0
isChecked={allRowsSelectedOnPage.isChecked} ? `${
isIndeterminate={allRowsSelectedOnPage.isIndeterminate} !allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
onCheckedChange={toggleSelectAllRows} } all folders and secrets on page`
: ""
}
>
<div className="ml-2 mr-4">
<Checkbox
isDisabled={totalCount === 0}
id="checkbox-select-all-rows"
isChecked={allRowsSelectedOnPage.isChecked}
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
onCheckedChange={toggleSelectAllRows}
/>
</div>
</Tooltip>
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={() =>
setOrderDirection((prev) =>
prev === OrderByDirection.ASC
? OrderByDirection.DESC
: OrderByDirection.ASC
)
}
>
<FontAwesomeIcon
icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
/> />
</div> </IconButton>
</Tooltip> </div>
Name <Tooltip
<IconButton content={
variant="plain" collapseEnvironments ? "Expand Environments" : "Collapse Environments"
className="ml-2"
ariaLabel="sort"
onClick={() =>
setOrderDirection((prev) =>
prev === OrderByDirection.ASC
? OrderByDirection.DESC
: OrderByDirection.ASC
)
} }
className="capitalize"
> >
<FontAwesomeIcon <IconButton
icon={orderDirection === "asc" ? faArrowDown : faArrowUp} ariaLabel="Toggle Environment View"
/> variant="plain"
</IconButton> colorSchema="secondary"
className="ml-auto mt-auto h-min p-1"
onClick={handleToggleNarrowHeader}
>
<FontAwesomeIcon
icon={collapseEnvironments ? faArrowLeft : faArrowRight}
/>
</IconButton>
</Tooltip>
</div> </div>
</Th> </Th>
{visibleEnvs?.map(({ name, slug }, index) => { {visibleEnvs?.map(({ name, slug }, index) => {
@@ -1223,28 +1272,76 @@ export const OverviewPage = () => {
return ( return (
<Th <Th
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center" className={twMerge(
"min-table-row border-b-0 p-0 text-xs",
collapseEnvironments && index === visibleEnvs.length - 1 && "mr-8",
collapseEnvironments ? "h-24 w-[1rem]" : "min-w-[11rem] text-center"
)}
key={`secret-overview-${name}-${index + 1}`} key={`secret-overview-${name}-${index + 1}`}
> >
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pb-[0.83rem] pt-3.5"> <Tooltip
<button content={
type="button" collapseEnvironments ? (
className="text-sm font-medium duration-100 hover:text-mineshaft-100" <p className="whitespace-break-spaces">{name}</p>
onClick={() => handleExploreEnvClick(slug)} ) : (
""
)
}
side="bottom"
sideOffset={-1}
align="end"
className="max-w-xl text-xs normal-case"
rootProps={{
disableHoverableContent: true
}}
>
<div
className={twMerge(
"border-b border-mineshaft-600",
collapseEnvironments
? "relative h-24 w-[2.9rem]"
: "flex items-center justify-center px-5 pb-[0.82rem] pt-3.5",
collapseEnvironments &&
index === visibleEnvs.length - 1 &&
"overflow-clip"
)}
> >
{name} <div
</button> className={twMerge(
{missingKeyCount > 0 && ( "border-mineshaft-600",
<Tooltip collapseEnvironments
className="max-w-none lowercase" ? "ml-[0.85rem] h-24 -skew-x-[16rad] transform border-l text-xs"
content={`${missingKeyCount} secrets missing\n compared to other environments`} : "flex items-center justify-center"
)}
/>
<button
type="button"
className={twMerge(
"duration-100 hover:text-mineshaft-100",
collapseEnvironments &&
(index === visibleEnvs.length - 1
? "bottom-[1.75rem] w-14"
: "bottom-10 w-20"),
collapseEnvironments
? "absolute -rotate-[72.25deg] text-left !text-[12px] font-normal"
: "flex items-center text-center text-sm font-medium"
)}
onClick={() => handleExploreEnvClick(slug)}
> >
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100"> <p className="truncate font-medium">{name}</p>
<span className="text-bunker-100">{missingKeyCount}</span> </button>
</div> {!collapseEnvironments && missingKeyCount > 0 && (
</Tooltip> <Tooltip
)} className="max-w-none lowercase"
</div> content={`${missingKeyCount} secrets missing\n compared to other environments`}
>
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
<span className="text-bunker-100">{missingKeyCount}</span>
</div>
</Tooltip>
)}
</div>
</Tooltip>
</Th> </Th>
); );
})} })}
@@ -1409,16 +1506,40 @@ export const OverviewPage = () => {
/> />
</Td> </Td>
{visibleEnvs?.map(({ name, slug }) => ( {visibleEnvs?.map(({ name, slug }) => (
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0"> <Td
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2"> key={`explore-${name}-btn`}
<Button className="border-0 border-r border-mineshaft-600 p-0"
size="xs" >
variant="outline_bg" <div
isFullWidth className={twMerge(
onClick={() => handleExploreEnvClick(slug)} "flex w-full items-center justify-center border-t border-mineshaft-600 py-2"
> )}
Explore >
</Button> {collapseEnvironments ? (
<Tooltip className="normal-case" content="Explore Environment">
<IconButton
ariaLabel="Explore Environment"
size="xs"
variant="outline_bg"
className="mx-auto h-[1.76rem] rounded"
onClick={() => handleExploreEnvClick(slug)}
>
<FontAwesomeIcon icon={faArrowRightToBracket} />
</IconButton>
</Tooltip>
) : (
<Button
leftIcon={
<FontAwesomeIcon className="mr-1" icon={faArrowRightToBracket} />
}
variant="outline_bg"
size="xs"
className="mx-2 w-full"
onClick={() => handleExploreEnvClick(slug)}
>
Explore
</Button>
)}
</div> </div>
</Td> </Td>
))} ))}

View File

@@ -36,7 +36,7 @@ export const SecretOverviewDynamicSecretRow = ({
isPresent ? "text-green-600" : "text-red-600" isPresent ? "text-green-600" : "text-red-600"
)} )}
> >
<div className="flex justify-center"> <div className="mx-auto flex w-[0.03rem] justify-center">
<FontAwesomeIcon <FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark} icon={isPresent ? faCheck : faXmark}

View File

@@ -70,7 +70,7 @@ export const SecretOverviewFolderRow = ({
isPresent ? "text-green-600" : "text-red-600" isPresent ? "text-green-600" : "text-red-600"
)} )}
> >
<div className="flex justify-center"> <div className="mx-auto flex w-[0.03rem] justify-center">
<FontAwesomeIcon <FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark} icon={isPresent ? faCheck : faXmark}

View File

@@ -94,7 +94,7 @@ export const SecretOverviewSecretRotationRow = ({
isPresent ? "text-green-600" : "text-red-600" isPresent ? "text-green-600" : "text-red-600"
)} )}
> >
<div className="flex justify-center"> <div className="mx-auto flex w-[0.03rem] justify-center">
<FontAwesomeIcon <FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark} icon={isPresent ? faCheck : faXmark}

View File

@@ -1,8 +1,8 @@
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
faCheck, faCheck,
faCircle,
faCodeBranch, faCodeBranch,
faEye, faEye,
faEyeSlash, faEyeSlash,
@@ -145,14 +145,14 @@ export const SecretOverviewTableRow = ({
<Td <Td
key={`sec-overview-${slug}-${i + 1}-value`} key={`sec-overview-${slug}-${i + 1}-value`}
className={twMerge( className={twMerge(
"px-0 py-0 group-hover:bg-mineshaft-700", "border-r border-mineshaft-600 px-0 py-3 group-hover:bg-mineshaft-700",
isFormExpanded && "border-t-2 border-mineshaft-500", isFormExpanded && "border-t-2 border-mineshaft-500",
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "", (isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "", isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : "" !isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
)} )}
> >
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]"> <div className="mx-auto flex w-[0.03rem] justify-center">
<div className="flex justify-center"> <div className="flex justify-center">
{!isSecretEmpty && ( {!isSecretEmpty && (
<Tooltip <Tooltip