diff --git a/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx b/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx index 2a6767396..69c469540 100644 --- a/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx +++ b/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; import { ViewMode } from "../types"; @@ -23,8 +24,11 @@ interface AccessTreeProviderProps { children: ReactNode; } +export type AccessTreeForm = { metadata: { key: string; value: string }[] }; + export const AccessTreeProvider: React.FC = ({ children }) => { const [secretName, setSecretName] = useState(""); + const formMethods = useForm({ defaultValues: { metadata: [] } }); const [viewMode, setViewMode] = useState(ViewMode.Docked); const value = useMemo( @@ -37,7 +41,11 @@ export const AccessTreeProvider: React.FC = ({ children [secretName, setSecretName, viewMode, setViewMode] ); - return {children}; + return ( + + {children} + + ); }; export const useAccessTreeContext = (): AccessTreeContextProps => { diff --git a/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx index 6e3790b3c..ee3ff1ff3 100644 --- a/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx +++ b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx @@ -1,10 +1,12 @@ import { Dispatch, SetStateAction, useState } from "react"; +import { useFormContext } from "react-hook-form"; import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Panel } from "@xyflow/react"; import { Button, FormLabel, IconButton, Input, Select, SelectItem } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; +import { MetadataForm } from "@app/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/MetadataForm"; import { ViewMode } from "../types"; @@ -32,6 +34,7 @@ export const PermissionSimulation = ({ setSecretName }: TProps) => { const [expand, setExpand] = useState(false); + const { control } = useFormContext(); const handlePermissionSimulation = () => { setExpand(true); @@ -139,6 +142,11 @@ export const PermissionSimulation = ({ /> )} + {subject === ProjectPermissionSub.DynamicSecrets && ( +
+ +
+ )} )} diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index a03f8fa0f..64e9e04a3 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useFormContext, useWatch } from "react-hook-form"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react"; @@ -7,7 +8,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries"; import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types"; -import { useAccessTreeContext } from "../components"; +import { AccessTreeForm, useAccessTreeContext } from "../components"; import { PermissionAccess } from "../types"; import { createBaseEdge, @@ -36,6 +37,8 @@ export const useAccessTree = ( ) => { const { currentWorkspace } = useWorkspace(); const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext(); + const { control } = useFormContext(); + const metadata = useWatch({ control, name: "metadata" }); const [nodes, setNodes] = useNodesState([]); const [edges, setEdges] = useEdgesState([]); const [subject, setSubject] = useState(ProjectPermissionSub.Secrets); @@ -168,7 +171,8 @@ export const useAccessTree = ( environment, subject, secretName, - actionRuleMap + actionRuleMap, + metadata }) ); @@ -266,7 +270,8 @@ export const useAccessTree = ( subject, secretName, setNodes, - setEdges + setEdges, + metadata ]); return { diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx index f2ca6e878..e96c6862e 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx @@ -17,6 +17,28 @@ type Props = { access: PermissionAccess; } & Pick["data"], "actionRuleMap" | "subject">; +type ConditionDisplayProps = { + _key: string; + operator: string; + value: string | string[]; + inverted?: boolean; +}; + +const Display = ({ _key: key, value, operator, inverted }: ConditionDisplayProps) => { + return ( +
  • + {camelCaseToSpaces(key)}{" "} + + {formatedConditionsOperatorNames[operator as PermissionConditionOperators]} + {" "} + + {typeof value === "string" ? value : value.join(", ")} + + . +
  • + ); +}; + export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subject }: Props) => { let component: ReactElement; @@ -56,43 +78,58 @@ export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subjec {actionRuleMap.map((ruleMap, index) => { const rule = ruleMap[action]; - if ( - !rule || - !rule.conditions || - (!rule.conditions.secretName && !rule.conditions.secretTags) - ) - return null; + if (!rule || !rule.conditions) return null; - return ( -
  • - - {rule.inverted ? "Forbids" : "Allows"} - - when: - {Object.entries(rule.conditions).map(([key, condition]) => ( -
      - {Object.entries(condition as object).map(([operator, value]) => ( -
    • - - {camelCaseToSpaces(key)} - {" "} - - { - formatedConditionsOperatorNames[ - operator as PermissionConditionOperators - ] + if ( + rule.conditions.secretName || + rule.conditions.secretTags || + rule.conditions.metadata + ) { + return ( +
    • + + {rule.inverted ? "Forbids" : "Allows"} + + when: + {Object.entries(rule.conditions).map(([key, condition]) => { + return ( +
        + {Object.entries(condition as object).map(([operator, value]) => { + if (operator === "$elemMatch") { + return Object.entries(value as object).map( + ([nestedKey, nestedCondition]) => + Object.entries(nestedCondition as object).map( + ([nestedOperator, nestedValue]) => ( + + ) + ) + ); } - {" "} - - {typeof value === "string" ? value : value.join(", ")} - - . - - ))} -
      - ))} -
    • - ); + + return ( + + ); + })} +
    + ); + })} +
  • + ); + } + + return null; })} diff --git a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx index f6ffe212e..c3e9f89e0 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from "react"; -import { faFileImport, faFolder, faKey, faLock } from "@fortawesome/free-solid-svg-icons"; +import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Handle, NodeProps, Position } from "@xyflow/react"; @@ -12,15 +12,15 @@ import { createRoleNode } from "../utils"; const getSubjectIcon = (subject: ProjectPermissionSub) => { switch (subject) { case ProjectPermissionSub.Secrets: - return ; + return ; case ProjectPermissionSub.SecretFolders: return ; case ProjectPermissionSub.DynamicSecrets: - return ; + return ; case ProjectPermissionSub.SecretImports: - return ; + return ; default: - return ; + return ; } }; diff --git a/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts index 15c64ce8a..8708812b1 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts @@ -58,7 +58,8 @@ export const createFolderNode = ({ environment, subject, secretName, - actionRuleMap + actionRuleMap, + metadata }: { folder: TSecretFolderWithPath; permissions: MongoAbility; @@ -66,6 +67,7 @@ export const createFolderNode = ({ subject: ProjectPermissionSub; secretName: string; actionRuleMap: TActionRuleMap; + metadata: Array<{ key: string; value: string }>; }) => { const actions = Object.fromEntries( Object.values(ACTION_MAP[subject] ?? Object.values(ProjectPermissionActions)).map((action) => { @@ -79,7 +81,8 @@ export const createFolderNode = ({ secretPath: folder.path, environment, secretName: secretName || "*", - secretTags: ["*"] + secretTags: ["*"], + metadata: metadata.length ? metadata : ["*"] }; if ( @@ -101,40 +104,98 @@ export const createFolderNode = ({ } if (hasPermission) { - // we want to show yellow/conditional access if user hasn't specified secret name to fully resolve access - if ( - !secretName && - actionRuleMap.some((el) => { - // we only show conditional if secretName/secretTags are present - environment and path can be directly determined - if (!el[action]?.conditions?.secretName && !el[action]?.conditions?.secretTags) - return false; - - // make sure condition applies to env - if (el[action]?.conditions?.environment) { - if ( - !Object.entries(el[action]?.conditions?.environment).every(([operator, value]) => - evaluateCondition(environment, operator as PermissionConditionOperators, value) - ) - ) { + if (subject === ProjectPermissionSub.Secrets) { + // we want to show yellow/conditional access if user hasn't specified secret name to fully resolve access + if ( + !secretName && + actionRuleMap.some((el) => { + // we only show conditional if secretName/secretTags are present - environment and path can be directly determined + if (!el[action]?.conditions?.secretName && !el[action]?.conditions?.secretTags) return false; - } - } - // and applies to path - if (el[action]?.conditions?.secretPath) { - if ( - !Object.entries(el[action]?.conditions?.secretPath).every(([operator, value]) => - evaluateCondition(folder.path, operator as PermissionConditionOperators, value) - ) - ) { - return false; + // make sure condition applies to env + if (el[action]?.conditions?.environment) { + if ( + !Object.entries(el[action]?.conditions?.environment).every( + ([operator, value]) => + evaluateCondition( + environment, + operator as PermissionConditionOperators, + value + ) + ) + ) { + return false; + } } - } - return true; - }) - ) { - access = PermissionAccess.Partial; + // and applies to path + if (el[action]?.conditions?.secretPath) { + if ( + !Object.entries(el[action]?.conditions?.secretPath).every(([operator, value]) => + evaluateCondition( + folder.path, + operator as PermissionConditionOperators, + value + ) + ) + ) { + return false; + } + } + + return true; + }) + ) { + access = PermissionAccess.Partial; + } else { + access = PermissionAccess.Full; + } + } else if (subject === ProjectPermissionSub.DynamicSecrets) { + if ( + !metadata.length && + actionRuleMap.some((el) => { + // we only show conditional if metadata present - environment and path can be directly determined + if (!el[action]?.conditions?.metadata) return false; + + // make sure condition applies to env + if (el[action]?.conditions?.environment) { + if ( + !Object.entries(el[action]?.conditions?.environment).every( + ([operator, value]) => + evaluateCondition( + environment, + operator as PermissionConditionOperators, + value + ) + ) + ) { + return false; + } + } + + // and applies to path + if (el[action]?.conditions?.secretPath) { + if ( + !Object.entries(el[action]?.conditions?.secretPath).every(([operator, value]) => + evaluateCondition( + folder.path, + operator as PermissionConditionOperators, + value + ) + ) + ) { + return false; + } + } + + return true; + }) + ) { + access = PermissionAccess.Partial; + } else { + access = PermissionAccess.Full; + } } else { access = PermissionAccess.Full; } diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index c82e492e8..a1d4fb2a2 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -137,7 +137,7 @@ export const SecretPathInput = ({ maxHeight: "var(--radix-select-content-available-height)" }} > -
    +
    {suggestions.map((suggestion, i) => (