Compare commits

...

6 Commits

Author SHA1 Message Date
Scott Wilson
f7b09f5fc2 improvement: add conditional display to access tree for dynamic secret metadata 2025-06-17 08:16:39 -07:00
Daniel Hougaard
9b13619efa Merge pull request #3799 from Infisical/daniel/hotfix-2
Fix: increase PIT tree checkout interval
2025-06-16 20:28:51 +04:00
Daniel Hougaard
c076a900dc Update env.ts 2025-06-16 20:27:02 +04:00
Daniel Hougaard
8a5279cf0d Merge pull request #3798 from Infisical/daniel/hotfix
fix: increase PIT checkpoint window
2025-06-16 20:09:29 +04:00
Daniel Hougaard
d45c29cd23 Update env.ts 2025-06-16 20:08:22 +04:00
carlosmonastyrski
46f9927cf1 Merge pull request #3796 from Infisical/fix/applyWorkspaceLimitToSecretManager
Add a condition to only limit the number of projects to SecretManager
2025-06-13 17:46:35 -03:00
8 changed files with 198 additions and 79 deletions

View File

@@ -262,8 +262,8 @@ const envSchema = z
DATADOG_HOSTNAME: zpStr(z.string().optional()),
// PIT
PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("2")),
PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("30")),
PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("100")),
PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("200")),
/* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr(

View File

@@ -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<AccessTreeProviderProps> = ({ children }) => {
const [secretName, setSecretName] = useState("");
const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } });
const [viewMode, setViewMode] = useState(ViewMode.Docked);
const value = useMemo(
@@ -37,7 +41,11 @@ export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children
[secretName, setSecretName, viewMode, setViewMode]
);
return <AccessTreeContext.Provider value={value}>{children}</AccessTreeContext.Provider>;
return (
<FormProvider {...formMethods}>
<AccessTreeContext.Provider value={value}>{children}</AccessTreeContext.Provider>
</FormProvider>
);
};
export const useAccessTreeContext = (): AccessTreeContextProps => {

View File

@@ -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 = ({
/>
</div>
)}
{subject === ProjectPermissionSub.DynamicSecrets && (
<div>
<MetadataForm control={control} />
</div>
)}
</>
)}
</div>

View File

@@ -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<AccessTreeForm>();
const metadata = useWatch({ control, name: "metadata" });
const [nodes, setNodes] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]);
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 {

View File

@@ -17,6 +17,28 @@ type Props = {
access: PermissionAccess;
} & Pick<ReturnType<typeof createFolderNode>["data"], "actionRuleMap" | "subject">;
type ConditionDisplayProps = {
_key: string;
operator: string;
value: string | string[];
inverted?: boolean;
};
const Display = ({ _key: key, value, operator, inverted }: ConditionDisplayProps) => {
return (
<li>
<span className="font-medium capitalize text-mineshaft-100">{camelCaseToSpaces(key)}</span>{" "}
<span className="text-mineshaft-200">
{formatedConditionsOperatorNames[operator as PermissionConditionOperators]}
</span>{" "}
<span className={inverted ? "text-red" : "text-green"}>
{typeof value === "string" ? value : value.join(", ")}
</span>
.
</li>
);
};
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 (
<li key={`${action}_${index + 1}`}>
<span className={`italic ${rule.inverted ? "text-red" : "text-green"} `}>
{rule.inverted ? "Forbids" : "Allows"}
</span>
<span> when:</span>
{Object.entries(rule.conditions).map(([key, condition]) => (
<ul key={`${action}_${index + 1}_${key}`} className="list-[square] pl-4">
{Object.entries(condition as object).map(([operator, value]) => (
<li key={`${action}_${index + 1}_${key}_${operator}`}>
<span className="font-medium capitalize text-mineshaft-100">
{camelCaseToSpaces(key)}
</span>{" "}
<span className="text-mineshaft-200">
{
formatedConditionsOperatorNames[
operator as PermissionConditionOperators
]
if (
rule.conditions.secretName ||
rule.conditions.secretTags ||
rule.conditions.metadata
) {
return (
<li key={`${action}_${index + 1}`}>
<span className={`italic ${rule.inverted ? "text-red" : "text-green"} `}>
{rule.inverted ? "Forbids" : "Allows"}
</span>
<span> when:</span>
{Object.entries(rule.conditions).map(([key, condition]) => {
return (
<ul key={`${action}_${index + 1}_${key}`} className="list-[square] pl-4">
{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]) => (
<Display
inverted={rule.inverted}
_key={`${key} ${nestedKey}`}
operator={nestedOperator}
value={nestedValue}
key={`${action}_${index + 1}_${key}_${operator}_${nestedKey}_${nestedOperator}`}
/>
)
)
);
}
</span>{" "}
<span className={rule.inverted ? "text-red" : "text-green"}>
{typeof value === "string" ? value : value.join(", ")}
</span>
.
</li>
))}
</ul>
))}
</li>
);
return (
<Display
inverted={rule.inverted}
_key={key}
operator={operator}
value={value}
key={`${action}_${index + 1}_${key}_${operator}`}
/>
);
})}
</ul>
);
})}
</li>
);
}
return null;
})}
</ul>
</>

View File

@@ -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 <FontAwesomeIcon icon={faLock} className="h-4 w-4 text-yellow-700" />;
return <FontAwesomeIcon icon={faKey} className="h-4 w-4 text-bunker-300" />;
case ProjectPermissionSub.SecretFolders:
return <FontAwesomeIcon icon={faFolder} className="h-4 w-4 text-yellow-700" />;
case ProjectPermissionSub.DynamicSecrets:
return <FontAwesomeIcon icon={faKey} className="h-4 w-4 text-yellow-700" />;
return <FontAwesomeIcon icon={faFingerprint} className="h-4 w-4 text-yellow-700" />;
case ProjectPermissionSub.SecretImports:
return <FontAwesomeIcon icon={faFileImport} className="h-4 w-4 text-yellow-700" />;
return <FontAwesomeIcon icon={faFileImport} className="h-4 w-4 text-green-700" />;
default:
return <FontAwesomeIcon icon={faLock} className="h-4 w-4 text-yellow-700" />;
return <FontAwesomeIcon icon={faKey} className="h-4 w-4 text-bunker-300" />;
}
};

View File

@@ -58,7 +58,8 @@ export const createFolderNode = ({
environment,
subject,
secretName,
actionRuleMap
actionRuleMap,
metadata
}: {
folder: TSecretFolderWithPath;
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
@@ -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;
}

View File

@@ -137,7 +137,7 @@ export const SecretPathInput = ({
maxHeight: "var(--radix-select-content-available-height)"
}}
>
<div className="max-h-[25vh] w-full flex-col items-center justify-center overflow-y-scroll rounded-md text-white">
<div className="thin-scrollbar max-h-[25vh] w-full flex-col items-center justify-center overflow-y-scroll rounded-md text-white">
{suggestions.map((suggestion, i) => (
<div
tabIndex={0}