mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 01:27:41 +00:00
Merge remote-tracking branch 'origin/main' into feat/pki-ENG-3666
This commit is contained in:
@@ -29,17 +29,54 @@ const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/;
|
||||
export const hasSecretReference = (value: string | undefined) =>
|
||||
value ? INTERPOLATION_SYNTAX_REG.test(value) : false;
|
||||
|
||||
const createNodeId = (node: TSecretReferenceTraceNode): string =>
|
||||
`${node.environment}:${node.secretPath}:${node.key}`;
|
||||
|
||||
const isCircularReference = (
|
||||
node: TSecretReferenceTraceNode,
|
||||
visitedPath: Set<string>
|
||||
): boolean => {
|
||||
const nodeId = createNodeId(node);
|
||||
return visitedPath.has(nodeId);
|
||||
};
|
||||
|
||||
const hasCircularReferences = (
|
||||
node: TSecretReferenceTraceNode,
|
||||
visitedPath: Set<string> = new Set()
|
||||
): boolean => {
|
||||
const nodeId = createNodeId(node);
|
||||
|
||||
if (visitedPath.has(nodeId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const newVisitedPath = new Set([...visitedPath, nodeId]);
|
||||
|
||||
return node.children.some((child) => hasCircularReferences(child, newVisitedPath));
|
||||
};
|
||||
|
||||
export const SecretReferenceNode = ({
|
||||
node,
|
||||
isRoot,
|
||||
secretKey
|
||||
secretKey,
|
||||
visitedPath = new Set()
|
||||
}: {
|
||||
node: TSecretReferenceTraceNode;
|
||||
isRoot?: boolean;
|
||||
secretKey?: string;
|
||||
visitedPath?: Set<string>;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
const nodeId = createNodeId(node);
|
||||
const isCircular = !isRoot && isCircularReference(node, visitedPath);
|
||||
|
||||
const newVisitedPath = isCircular ? visitedPath : new Set([...visitedPath, nodeId]);
|
||||
|
||||
const safeChildren = isCircular
|
||||
? []
|
||||
: node.children.filter((child) => !isCircularReference(child, newVisitedPath));
|
||||
const hasChildren = safeChildren.length > 0;
|
||||
|
||||
return (
|
||||
<li>
|
||||
@@ -77,8 +114,12 @@ export const SecretReferenceNode = ({
|
||||
<Collapsible.Content className={twMerge("mt-4", style.collapsibleContent)}>
|
||||
{hasChildren && (
|
||||
<ul>
|
||||
{node.children.map((el, index) => (
|
||||
<SecretReferenceNode node={el} key={`${el.key}-${index + 1}`} />
|
||||
{safeChildren.map((el, index) => (
|
||||
<SecretReferenceNode
|
||||
node={el}
|
||||
key={`${el.key}-${index + 1}`}
|
||||
visitedPath={newVisitedPath}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
@@ -102,6 +143,9 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
const tree = data?.tree;
|
||||
const secretValue = data?.value;
|
||||
|
||||
// Check if the tree contains circular references
|
||||
const hasCirculars = tree ? hasCircularReferences(tree) : false;
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof AxiosError) {
|
||||
const err = error?.response?.data as TApiErrors;
|
||||
@@ -130,9 +174,25 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
);
|
||||
}
|
||||
|
||||
if (tree?.children?.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<span className="text-mineshaft-400">This secret does not contain references</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControl label="Expanded value">
|
||||
<FormControl
|
||||
label="Expanded value"
|
||||
tooltipText={
|
||||
hasCirculars
|
||||
? "This secret contains circular references. Value shown is resolved once, with circular paths truncated in the reference tree below."
|
||||
: undefined
|
||||
}
|
||||
tooltipClassName="max-w-md break-words"
|
||||
>
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isReadOnly
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFolder, faKey, faLayerGroup, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
|
||||
@@ -55,6 +55,8 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
|
||||
secretPath?: string;
|
||||
environment?: string;
|
||||
containerClassName?: string;
|
||||
isLoadingValue?: boolean;
|
||||
isErrorLoadingValue?: boolean;
|
||||
};
|
||||
|
||||
type ReferenceItem = {
|
||||
@@ -175,13 +177,29 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
type: ReferenceType.SECRET
|
||||
});
|
||||
});
|
||||
|
||||
if (suggestionsArr.length === 0 && suggestionSource.predicate.trim()) {
|
||||
suggestionsArr.push({
|
||||
label: "No matches found",
|
||||
slug: "__no_match__",
|
||||
type: ReferenceType.SECRET
|
||||
});
|
||||
}
|
||||
|
||||
return suggestionsArr;
|
||||
}, [secrets, folders, currentProject?.environments, isPopupOpen, suggestionSource.value]);
|
||||
}, [
|
||||
secrets,
|
||||
folders,
|
||||
currentProject?.environments,
|
||||
isPopupOpen,
|
||||
suggestionSource.value,
|
||||
suggestionSource.predicate
|
||||
]);
|
||||
|
||||
const handleSuggestionSelect = (selectIndex?: number) => {
|
||||
const selectedSuggestion =
|
||||
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
|
||||
if (!selectedSuggestion) {
|
||||
if (!selectedSuggestion || selectedSuggestion.slug === "__no_match__") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,21 +244,40 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
if (isPopupOpen) {
|
||||
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
|
||||
setHighlightedIndex((prevIndex) => {
|
||||
const pos = mod(prevIndex + 1, suggestions.length);
|
||||
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
|
||||
let nextIndex = mod(prevIndex + 1, suggestions.length);
|
||||
// Skip "no match" messages
|
||||
while (
|
||||
nextIndex < suggestions.length &&
|
||||
suggestions[nextIndex].slug === "__no_match__"
|
||||
) {
|
||||
nextIndex = mod(nextIndex + 1, suggestions.length);
|
||||
}
|
||||
// If we only have no-match messages, don't highlight anything
|
||||
if (suggestions[nextIndex]?.slug === "__no_match__") {
|
||||
return -1;
|
||||
}
|
||||
popoverContentRef.current?.children?.[nextIndex]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth"
|
||||
});
|
||||
return pos;
|
||||
return nextIndex;
|
||||
});
|
||||
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
|
||||
setHighlightedIndex((prevIndex) => {
|
||||
const pos = mod(prevIndex - 1, suggestions.length);
|
||||
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
|
||||
let prevIdx = mod(prevIndex - 1, suggestions.length);
|
||||
// Skip "no match" messages
|
||||
while (prevIdx >= 0 && suggestions[prevIdx].slug === "__no_match__") {
|
||||
prevIdx = mod(prevIdx - 1, suggestions.length);
|
||||
}
|
||||
// If we only have no-match messages, don't highlight anything
|
||||
if (suggestions[prevIdx]?.slug === "__no_match__") {
|
||||
return -1;
|
||||
}
|
||||
popoverContentRef.current?.children?.[prevIdx]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth"
|
||||
});
|
||||
return pos;
|
||||
return prevIdx;
|
||||
});
|
||||
} else if (e.key === "Enter" && highlightedIndex >= 0) {
|
||||
e.preventDefault();
|
||||
@@ -279,11 +316,16 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
ref={handleRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={value}
|
||||
onFocus={() => setIsFocused.on()}
|
||||
onFocus={(evt) => {
|
||||
if (props.onFocus) props.onFocus(evt);
|
||||
setIsFocused.on();
|
||||
}}
|
||||
onBlur={(evt) => {
|
||||
// should not on blur when its mouse down selecting a item from suggestion
|
||||
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
|
||||
setIsFocused.off();
|
||||
|
||||
if (props.onBlur) props.onBlur(evt);
|
||||
}}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
containerClassName={containerClassName}
|
||||
@@ -304,7 +346,12 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
{suggestions.map((item, i) => {
|
||||
let entryIcon;
|
||||
let subText;
|
||||
if (item.type === ReferenceType.SECRET) {
|
||||
const isNoMatchMessage = item.slug === "__no_match__";
|
||||
|
||||
if (isNoMatchMessage) {
|
||||
entryIcon = <FontAwesomeIcon icon={faSearch} className="text-gray-400" />;
|
||||
subText = "No results";
|
||||
} else if (item.type === ReferenceType.SECRET) {
|
||||
entryIcon = <FontAwesomeIcon icon={faKey} className="text-bunker-300" />;
|
||||
subText = "Secret";
|
||||
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
||||
@@ -315,10 +362,28 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
subText = "Folder";
|
||||
}
|
||||
|
||||
return (
|
||||
return isNoMatchMessage ? (
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
role="status"
|
||||
aria-label="no-match-message"
|
||||
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
|
||||
key={`secret-reference-secret-${i + 1}`}
|
||||
>
|
||||
<div className="text-md relative flex w-full cursor-default select-none items-center justify-between px-2 py-2 opacity-75 outline-none transition-all">
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<div className="mt-1 flex items-center">{entryIcon}</div>
|
||||
<div className="text-md w-10/12 truncate text-left">
|
||||
<span className="text-gray-400">{item.label}</span>
|
||||
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
|
||||
{subText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSuggestionSelect(i);
|
||||
}}
|
||||
@@ -330,8 +395,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
handleSuggestionSelect(i);
|
||||
}}
|
||||
onMouseEnter={() => setHighlightedIndex(i)}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
|
||||
className="flex w-full items-center justify-between border-none border-mineshaft-600 bg-transparent p-0 text-left"
|
||||
key={`secret-reference-secret-${i + 1}`}
|
||||
>
|
||||
<div
|
||||
@@ -342,14 +406,14 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<div className="mt-1 flex items-center">{entryIcon}</div>
|
||||
<div className="text-md w-10/12 truncate text-left">
|
||||
{item.label}
|
||||
<span>{item.label}</span>
|
||||
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
|
||||
{subText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,16 @@ import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPa
|
||||
|
||||
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
|
||||
|
||||
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
|
||||
const syntaxHighlight = (
|
||||
content?: string | null,
|
||||
isVisible?: boolean,
|
||||
isImport?: boolean,
|
||||
isLoadingValue?: boolean,
|
||||
isErrorLoadingValue?: boolean
|
||||
) => {
|
||||
if (isLoadingValue) return HIDDEN_SECRET_VALUE;
|
||||
if (isErrorLoadingValue)
|
||||
return <span className="ph-no-capture text-red/75">Error loading secret value.</span>;
|
||||
if (isImport && !content) return "IMPORTED";
|
||||
if (content === "") return "EMPTY";
|
||||
if (!content) return "EMPTY";
|
||||
@@ -20,7 +29,8 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
|
||||
skipNext = true;
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
||||
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||
${
|
||||
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
@@ -48,6 +58,8 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
isDisabled?: boolean;
|
||||
containerClassName?: string;
|
||||
canEditButNotView?: boolean;
|
||||
isLoadingValue?: boolean;
|
||||
isErrorLoadingValue?: boolean;
|
||||
};
|
||||
|
||||
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
|
||||
@@ -65,6 +77,8 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
isReadOnly,
|
||||
onFocus,
|
||||
canEditButNotView,
|
||||
isLoadingValue,
|
||||
isErrorLoadingValue,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -83,7 +97,9 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
{syntaxHighlight(
|
||||
value,
|
||||
isVisible || (isSecretFocused && !valueAlwaysHidden),
|
||||
isImport
|
||||
isImport,
|
||||
isLoadingValue,
|
||||
isErrorLoadingValue
|
||||
)}
|
||||
</span>
|
||||
</code>
|
||||
@@ -114,7 +130,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
}}
|
||||
value={value || ""}
|
||||
{...props}
|
||||
readOnly={isReadOnly}
|
||||
readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,13 @@ export enum OrgGatewayPermissionActions {
|
||||
AttachGateways = "attach-gateways"
|
||||
}
|
||||
|
||||
export enum OrgRelayPermissionActions {
|
||||
CreateRelays = "create-relays",
|
||||
ListRelays = "list-relays",
|
||||
EditRelays = "edit-relays",
|
||||
DeleteRelays = "delete-relays"
|
||||
}
|
||||
|
||||
export enum OrgPermissionMachineIdentityAuthTemplateActions {
|
||||
ListTemplates = "list-templates",
|
||||
CreateTemplates = "create-templates",
|
||||
@@ -51,6 +58,7 @@ export enum OrgPermissionSubjects {
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway",
|
||||
Relay = "relay",
|
||||
SecretShare = "secret-share",
|
||||
GithubOrgSync = "github-org-sync",
|
||||
GithubOrgSyncManual = "github-org-sync-manual",
|
||||
@@ -135,6 +143,7 @@ export type OrgPermissionSet =
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
]
|
||||
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
|
||||
| [OrgRelayPermissionActions, OrgPermissionSubjects.Relay]
|
||||
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]
|
||||
| [
|
||||
OrgPermissionAppConnectionActions,
|
||||
|
||||
@@ -230,7 +230,23 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.UPDATE_ORG]: "Update Organization",
|
||||
[EventType.CREATE_PROJECT]: "Create Project",
|
||||
[EventType.UPDATE_PROJECT]: "Update Project",
|
||||
[EventType.DELETE_PROJECT]: "Delete Project"
|
||||
[EventType.DELETE_PROJECT]: "Delete Project",
|
||||
|
||||
[EventType.CREATE_SECRET_REMINDER]: "Create Secret Reminder",
|
||||
[EventType.GET_SECRET_REMINDER]: "Get Secret Reminder",
|
||||
[EventType.DELETE_SECRET_REMINDER]: "Delete Secret Reminder",
|
||||
|
||||
[EventType.DASHBOARD_LIST_SECRETS]: "Dashboard List Secrets",
|
||||
[EventType.DASHBOARD_GET_SECRET_VALUE]: "Dashboard Get Secret Value",
|
||||
[EventType.DASHBOARD_GET_SECRET_VERSION_VALUE]: "Dashboard Get Secret Version Value",
|
||||
|
||||
[EventType.CREATE_PROJECT_ROLE]: "Create Project Role",
|
||||
[EventType.UPDATE_PROJECT_ROLE]: "Update Project Role",
|
||||
[EventType.DELETE_PROJECT_ROLE]: "Delete Project Role",
|
||||
|
||||
[EventType.CREATE_ORG_ROLE]: "Create Org Role",
|
||||
[EventType.UPDATE_ORG_ROLE]: "Update Org Role",
|
||||
[EventType.DELETE_ORG_ROLE]: "Delete Org Role"
|
||||
};
|
||||
|
||||
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -224,5 +224,21 @@ export enum EventType {
|
||||
|
||||
CREATE_PROJECT = "create-project",
|
||||
UPDATE_PROJECT = "update-project",
|
||||
DELETE_PROJECT = "delete-project"
|
||||
DELETE_PROJECT = "delete-project",
|
||||
|
||||
CREATE_SECRET_REMINDER = "create-secret-reminder",
|
||||
GET_SECRET_REMINDER = "get-secret-reminder",
|
||||
DELETE_SECRET_REMINDER = "delete-secret-reminder",
|
||||
|
||||
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
|
||||
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
|
||||
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value",
|
||||
|
||||
CREATE_PROJECT_ROLE = "create-project-role",
|
||||
UPDATE_PROJECT_ROLE = "update-project-role",
|
||||
DELETE_PROJECT_ROLE = "delete-project-role",
|
||||
|
||||
CREATE_ORG_ROLE = "create-org-role",
|
||||
UPDATE_ORG_ROLE = "update-org-role",
|
||||
DELETE_ORG_ROLE = "delete-org-role"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
@@ -10,13 +10,15 @@ import {
|
||||
DashboardProjectSecretsOverview,
|
||||
DashboardProjectSecretsOverviewResponse,
|
||||
DashboardSecretsOrderBy,
|
||||
DashboardSecretValue,
|
||||
TDashboardProjectSecretsQuickSearch,
|
||||
TDashboardProjectSecretsQuickSearchResponse,
|
||||
TGetAccessibleSecretsDTO,
|
||||
TGetDashboardProjectSecretsByKeys,
|
||||
TGetDashboardProjectSecretsDetailsDTO,
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
TGetDashboardProjectSecretsQuickSearchDTO
|
||||
TGetDashboardProjectSecretsQuickSearchDTO,
|
||||
TGetSecretValueDTO
|
||||
} from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
||||
@@ -73,6 +75,15 @@ export const dashboardKeys = {
|
||||
...dashboardKeys.all(),
|
||||
"accessible-secrets",
|
||||
{ projectId, secretPath, environment, filterByAction }
|
||||
] as const,
|
||||
getSecretValuesRoot: () => [...dashboardKeys.all(), "secrets-values"] as const,
|
||||
getSecretValue: ({ environment, secretPath, secretKey, isOverride }: TGetSecretValueDTO) =>
|
||||
[
|
||||
...dashboardKeys.getSecretValuesRoot(),
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey,
|
||||
isOverride
|
||||
] as const
|
||||
};
|
||||
|
||||
@@ -174,6 +185,8 @@ export const useGetProjectSecretsOverview = (
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery({
|
||||
...options,
|
||||
// wait for all values to be available
|
||||
@@ -193,8 +206,8 @@ export const useGetProjectSecretsOverview = (
|
||||
includeSecretRotations,
|
||||
environments
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchProjectSecretsOverview({
|
||||
queryFn: async () => {
|
||||
const resp = fetchProjectSecretsOverview({
|
||||
secretPath,
|
||||
search,
|
||||
limit,
|
||||
@@ -208,7 +221,14 @@ export const useGetProjectSecretsOverview = (
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
environments
|
||||
}),
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getSecretValuesRoot()
|
||||
});
|
||||
|
||||
return resp;
|
||||
},
|
||||
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
|
||||
const { secrets, secretRotations, ...select } = data;
|
||||
const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : [];
|
||||
@@ -254,7 +274,6 @@ export const useGetProjectSecretsDetails = (
|
||||
search = "",
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
viewSecretValue,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
@@ -270,6 +289,8 @@ export const useGetProjectSecretsDetails = (
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery({
|
||||
...options,
|
||||
// wait for all values to be available
|
||||
@@ -286,7 +307,6 @@ export const useGetProjectSecretsDetails = (
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
viewSecretValue,
|
||||
offset,
|
||||
projectId,
|
||||
environment,
|
||||
@@ -297,14 +317,13 @@ export const useGetProjectSecretsDetails = (
|
||||
includeSecretRotations,
|
||||
tags
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchProjectSecretsDetails({
|
||||
queryFn: async () => {
|
||||
const resp = await fetchProjectSecretsDetails({
|
||||
secretPath,
|
||||
search,
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
viewSecretValue,
|
||||
offset,
|
||||
projectId,
|
||||
environment,
|
||||
@@ -314,7 +333,14 @@ export const useGetProjectSecretsDetails = (
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
tags
|
||||
}),
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getSecretValuesRoot()
|
||||
});
|
||||
|
||||
return resp;
|
||||
},
|
||||
select: useCallback(
|
||||
(data: Awaited<ReturnType<typeof fetchProjectSecretsDetails>>) => ({
|
||||
...data,
|
||||
@@ -471,3 +497,31 @@ export const useGetAccessibleSecrets = ({
|
||||
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive })
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchSecretValue = async (params: TGetSecretValueDTO) => {
|
||||
const { data } = await apiRequest.get<DashboardSecretValue>("/api/v1/dashboard/secret-value", {
|
||||
params
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetSecretValue = (
|
||||
params: TGetSecretValueDTO,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
DashboardSecretValue,
|
||||
unknown,
|
||||
DashboardSecretValue,
|
||||
ReturnType<typeof dashboardKeys.getSecretValue>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.getSecretValue(params),
|
||||
queryFn: async () => fetchSecretValue(params),
|
||||
staleTime: 1000 * 60,
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -112,7 +112,6 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
"environments"
|
||||
> & {
|
||||
viewSecretValue: boolean;
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
tags: Record<string, boolean>;
|
||||
@@ -156,3 +155,21 @@ export type TGetAccessibleSecretsDTO = {
|
||||
| ProjectPermissionSecretActions.DescribeSecret
|
||||
| ProjectPermissionSecretActions.ReadValue;
|
||||
};
|
||||
|
||||
export type TGetSecretValueDTO = {
|
||||
projectId: string;
|
||||
secretKey: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
isOverride?: boolean;
|
||||
};
|
||||
|
||||
export type DashboardSecretValue =
|
||||
| {
|
||||
value: string;
|
||||
valueOverride: undefined;
|
||||
}
|
||||
| {
|
||||
value: undefined;
|
||||
valueOverride: string;
|
||||
};
|
||||
|
||||
@@ -59,6 +59,11 @@ export enum DynamicSecretAwsIamAuth {
|
||||
IRSA = "irsa"
|
||||
}
|
||||
|
||||
export enum DynamicSecretAwsIamCredentialType {
|
||||
IamUser = "iam-user",
|
||||
TemporaryCredentials = "temporary-credentials"
|
||||
}
|
||||
|
||||
export type TDynamicSecretProvider =
|
||||
| {
|
||||
type: DynamicSecretProviders.SqlDatabase;
|
||||
@@ -97,6 +102,7 @@ export type TDynamicSecretProvider =
|
||||
inputs:
|
||||
| {
|
||||
method: DynamicSecretAwsIamAuth.AccessKey;
|
||||
credentialType: DynamicSecretAwsIamCredentialType;
|
||||
accessKey: string;
|
||||
secretAccessKey: string;
|
||||
region: string;
|
||||
@@ -107,6 +113,7 @@ export type TDynamicSecretProvider =
|
||||
}
|
||||
| {
|
||||
method: DynamicSecretAwsIamAuth.AssumeRole;
|
||||
credentialType: DynamicSecretAwsIamCredentialType;
|
||||
roleArn: string;
|
||||
region: string;
|
||||
awsPath?: string;
|
||||
@@ -116,6 +123,7 @@ export type TDynamicSecretProvider =
|
||||
}
|
||||
| {
|
||||
method: DynamicSecretAwsIamAuth.IRSA;
|
||||
credentialType: DynamicSecretAwsIamCredentialType;
|
||||
region: string;
|
||||
awsPath?: string;
|
||||
policyDocument?: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { relayQueryKeys } from "./queries";
|
||||
|
||||
export const useDeleteRelayById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
return apiRequest.delete(`/api/v1/relays/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: relayQueryKeys.list() });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TRelay } from "./types";
|
||||
|
||||
export const relayQueryKeys = {
|
||||
list: () => ["relays"] as const
|
||||
};
|
||||
|
||||
const fetchRelays = async (): Promise<TRelay[]> => {
|
||||
const { data } = await apiRequest.get<TRelay[]>("/api/v1/relays");
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetRelays = () => {
|
||||
return useQuery({
|
||||
queryKey: relayQueryKeys.list(),
|
||||
queryFn: fetchRelays
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export type TRelay = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
orgId: string | null;
|
||||
identityId: string | null;
|
||||
name: string;
|
||||
host: string;
|
||||
};
|
||||
|
||||
export type TDeleteRelayDTO = {
|
||||
id: string;
|
||||
};
|
||||
@@ -67,7 +67,7 @@ export const useGetSecretImports = ({
|
||||
|
||||
const fetchImportedSecrets = async (projectId: string, environment: string, directory?: string) => {
|
||||
const { data } = await apiRequest.get<{ secrets: TImportedSecrets[] }>(
|
||||
"/api/v2/secret-imports/secrets",
|
||||
"/api/v1/dashboard/secret-imports",
|
||||
{
|
||||
params: {
|
||||
projectId,
|
||||
@@ -132,13 +132,13 @@ export const useGetImportedSecretsSingleEnv = ({
|
||||
id: encSecret.id,
|
||||
env: encSecret.environment,
|
||||
key: encSecret.secretKey,
|
||||
value: encSecret.secretValue,
|
||||
secretValueHidden: encSecret.secretValueHidden,
|
||||
tags: encSecret.tags,
|
||||
comment: encSecret.secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt,
|
||||
version: encSecret.version
|
||||
version: encSecret.version,
|
||||
isEmpty: encSecret.isEmpty
|
||||
};
|
||||
})
|
||||
}));
|
||||
@@ -172,7 +172,6 @@ export const useGetImportedSecretsAllEnvs = ({
|
||||
id: encSecret.id,
|
||||
env: encSecret.environment,
|
||||
key: encSecret.secretKey,
|
||||
value: encSecret.secretValue,
|
||||
secretValueHidden: encSecret.secretValueHidden,
|
||||
tags: encSecret.tags,
|
||||
comment: encSecret.secretComment,
|
||||
@@ -233,7 +232,9 @@ export const useGetImportedSecretsAllEnvs = ({
|
||||
|
||||
return {
|
||||
secret: secret?.secrets.find((s) => s.key === secretName),
|
||||
environmentInfo: secret?.environmentInfo
|
||||
environmentInfo: secret?.environmentInfo,
|
||||
secretPath: secret?.secretPath,
|
||||
environment: secret?.environment
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -28,7 +28,7 @@ export type TImportedSecrets = {
|
||||
environmentInfo: ProjectEnv;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: SecretV3Raw[];
|
||||
secrets: Omit<SecretV3Raw, "secretValue">[];
|
||||
};
|
||||
|
||||
export type TGetSecretImports = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import axios from "axios";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
import { ERROR_NOT_ALLOWED_READ_SECRETS } from "./constants";
|
||||
import {
|
||||
@@ -21,7 +22,9 @@ import {
|
||||
TGetProjectSecretsKey,
|
||||
TGetSecretAccessListDTO,
|
||||
TGetSecretReferenceTreeDTO,
|
||||
TSecretReferenceTraceNode
|
||||
TGetSecretVersionValue,
|
||||
TSecretReferenceTraceNode,
|
||||
TSecretVersionValue
|
||||
} from "./types";
|
||||
|
||||
export const secretKeys = {
|
||||
@@ -34,6 +37,8 @@ export const secretKeys = {
|
||||
}: TGetProjectSecretsKey) =>
|
||||
[{ projectId, environment, secretPath, viewSecretValue }, "secrets"] as const,
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
|
||||
getSecretVersionValue: (secretId: string, version: number) =>
|
||||
["secret-versions", secretId, version] as const,
|
||||
getSecretAccessList: ({
|
||||
projectId,
|
||||
environment,
|
||||
@@ -67,14 +72,17 @@ export const fetchProjectSecrets = async ({
|
||||
};
|
||||
|
||||
export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
|
||||
const personalSecrets: Record<string, { id: string; value?: string; env: string }> = {};
|
||||
const personalSecrets: Record<
|
||||
string,
|
||||
{ id: string; value?: string; env: string; isEmpty?: boolean }
|
||||
> = {};
|
||||
const secrets: SecretV3RawSanitized[] = [];
|
||||
rawSecrets.forEach((el) => {
|
||||
const decryptedSecret: SecretV3RawSanitized = {
|
||||
id: el.id,
|
||||
env: el.environment,
|
||||
key: el.secretKey,
|
||||
value: el.secretValue,
|
||||
value: el.secretValueHidden ? HIDDEN_SECRET_VALUE : el.secretValue,
|
||||
secretValueHidden: el.secretValueHidden,
|
||||
tags: el.tags || [],
|
||||
comment: el.secretComment || "",
|
||||
@@ -89,14 +97,16 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
|
||||
secretMetadata: el.secretMetadata,
|
||||
isRotatedSecret: el.isRotatedSecret,
|
||||
rotationId: el.rotationId,
|
||||
reminder: el.reminder
|
||||
reminder: el.reminder,
|
||||
isEmpty: el.isEmpty
|
||||
};
|
||||
|
||||
if (el.type === SecretType.Personal) {
|
||||
personalSecrets[decryptedSecret.key] = {
|
||||
id: el.id,
|
||||
value: el.secretValue,
|
||||
env: el.environment
|
||||
env: el.environment,
|
||||
isEmpty: el.isEmpty
|
||||
};
|
||||
} else {
|
||||
secrets.push(decryptedSecret);
|
||||
@@ -109,6 +119,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
|
||||
sec.idOverride = personalSecret.id;
|
||||
sec.valueOverride = personalSecret.value;
|
||||
sec.overrideAction = "modified";
|
||||
sec.isEmpty = personalSecret.isEmpty;
|
||||
sec.secretValueHidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -238,7 +250,7 @@ export const useGetProjectSecretsAllEnv = ({
|
||||
|
||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||
const { data } = await apiRequest.get<{ secretVersions: SecretVersions[] }>(
|
||||
`/api/v1/secret/${secretId}/secret-versions`,
|
||||
`/api/v1/dashboard/secret-versions/${secretId}`,
|
||||
{
|
||||
params: {
|
||||
limit,
|
||||
@@ -259,6 +271,26 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
||||
}, [])
|
||||
});
|
||||
|
||||
export const fetchSecretVersionValue = async (secretId: string, version: number) => {
|
||||
const { data } = await apiRequest.get<TSecretVersionValue>(
|
||||
`/api/v1/dashboard/secret-versions/${secretId}/value/${version}`
|
||||
);
|
||||
return data.value;
|
||||
};
|
||||
|
||||
export const useGetSecretVersionValue = (
|
||||
dto: TGetSecretVersionValue,
|
||||
options?: Omit<
|
||||
UseQueryOptions<string, unknown, string, ReturnType<typeof secretKeys.getSecretVersionValue>>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) =>
|
||||
useQuery({
|
||||
queryKey: secretKeys.getSecretVersionValue(dto.secretId, dto.version),
|
||||
queryFn: () => fetchSecretVersionValue(dto.secretId, dto.version),
|
||||
...options
|
||||
});
|
||||
|
||||
export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) =>
|
||||
useQuery({
|
||||
enabled: Boolean(dto.secretKey),
|
||||
|
||||
@@ -47,6 +47,7 @@ export type SecretV3RawSanitized = {
|
||||
isPending?: boolean;
|
||||
pendingAction?: PendingAction;
|
||||
reminder?: Reminder;
|
||||
isEmpty?: boolean;
|
||||
};
|
||||
|
||||
export type SecretV3Raw = {
|
||||
@@ -73,6 +74,7 @@ export type SecretV3Raw = {
|
||||
rotationId?: string;
|
||||
secretReminderRecipients?: SecretReminderRecipient[];
|
||||
reminder?: Reminder;
|
||||
isEmpty?: boolean;
|
||||
};
|
||||
|
||||
export type SecretV3RawResponse = {
|
||||
@@ -137,6 +139,15 @@ export type GetSecretVersionsDTO = {
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export type TGetSecretVersionValue = {
|
||||
secretId: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
export type TSecretVersionValue = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TGetSecretAccessListDTO = {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
|
||||
@@ -26,7 +26,11 @@ export const Notification = ({ notification, onDelete }: Props) => {
|
||||
{!notification.isRead && (
|
||||
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
|
||||
)}
|
||||
<Tooltip content={<Markdown>{notification.title}</Markdown>} delayDuration={300}>
|
||||
<Tooltip
|
||||
content={<Markdown>{notification.title}</Markdown>}
|
||||
delayDuration={300}
|
||||
className="z-[1000]"
|
||||
>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5 text-mineshaft-100">
|
||||
<Markdown components={{ p: "span" }}>{notification.title}</Markdown>
|
||||
</span>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const NotificationDropdown = () => {
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
side="bottom"
|
||||
className="mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
|
||||
className="z-[999] mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-500 px-3 py-2">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
faBook,
|
||||
faCog,
|
||||
faDoorClosed,
|
||||
faInfinity,
|
||||
faMoneyBill,
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
faShare,
|
||||
faTable,
|
||||
@@ -124,14 +124,14 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/gateways">
|
||||
<Link to="/organization/networking">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faDoorClosed} className="mr-4" />
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mr-4" />
|
||||
</div>
|
||||
Gateways
|
||||
Networking
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
|
||||
const { currentProject } = useProject();
|
||||
const { data, isPending } = useListWorkspaceCertificates({
|
||||
projectId: currentProject?.slug ?? "",
|
||||
projectId: currentProject?.id ?? "",
|
||||
offset: (page - 1) * perPage,
|
||||
limit: perPage
|
||||
});
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCopy,
|
||||
faDoorClosed,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faInfoCircle,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
PageHeader,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
|
||||
import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
|
||||
|
||||
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
|
||||
|
||||
export const GatewayListPage = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteGateway",
|
||||
"editDetails"
|
||||
] as const);
|
||||
|
||||
const deleteGatewayById = useDeleteGatewayById();
|
||||
const deleteGatewayV2ById = useDeleteGatewayV2ById();
|
||||
|
||||
const handleDeleteGateway = async () => {
|
||||
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
|
||||
if (data.isV1) {
|
||||
await deleteGatewayById.mutateAsync(data.id);
|
||||
} else {
|
||||
await deleteGatewayV2ById.mutateAsync(data.id);
|
||||
}
|
||||
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted gateway"
|
||||
});
|
||||
};
|
||||
|
||||
const filteredGateway = gateways?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>Infisical | Gateways</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
className="w-full"
|
||||
title={
|
||||
<div className="flex w-full items-center">
|
||||
<span>Gateways</span>
|
||||
<a
|
||||
className="-mt-1.5"
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
description="Create and configure gateway to access private network resources from Infisical"
|
||||
/>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search gateway..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Identity</Th>
|
||||
<Th>
|
||||
Health Check
|
||||
<Tooltip
|
||||
asChild={false}
|
||||
className="normal-case"
|
||||
content="The last known healthcheck. Triggers every 1 hour."
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isGatewaysLoading && (
|
||||
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
|
||||
)}
|
||||
{filteredGateway?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Gateway v{el.isV1 ? "1" : "2"}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{el.identity.name}</Td>
|
||||
<Td>
|
||||
{el.heartbeat
|
||||
? formatRelative(new Date(el.heartbeat), new Date())
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => navigator.clipboard.writeText(el.id)}
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
{el.isV1 && (
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionAppConnectionActions.Delete}
|
||||
a={OrgPermissionSubjects.AppConnections}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteGateway", el)}
|
||||
>
|
||||
Delete Gateway
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
<Modal
|
||||
isOpen={popUp.editDetails.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||
>
|
||||
<ModalContent title="Edit Gateway">
|
||||
<EditGatewayDetailsModal
|
||||
gatewayDetails={popUp.editDetails.data}
|
||||
onClose={() => handlePopUpToggle("editDetails")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isGatewaysLoading && !filteredGateway?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
gateways?.length
|
||||
? "No Gateways match search..."
|
||||
: "No Gateways have been configured"
|
||||
}
|
||||
icon={gateways?.length ? faSearch : faDoorClosed}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGateway.isOpen}
|
||||
title={`Are you sure you want to delete gateway ${
|
||||
(popUp?.deleteGateway?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteGateway()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { GatewayListPage } from "./GatewayListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
|
||||
)({
|
||||
component: GatewayListPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Gateways"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup";
|
||||
|
||||
export const NetworkingPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Infisical | Networking</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Networking"
|
||||
description="Manage gateways and relays to securely access private network resources from Infisical"
|
||||
/>
|
||||
<NetworkingTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,266 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCopy,
|
||||
faDoorClosed,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faInfoCircle,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
|
||||
import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
|
||||
|
||||
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
|
||||
|
||||
const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
|
||||
const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
|
||||
const now = new Date();
|
||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
const isHealthy = heartbeatDate && heartbeatDate >= oneHourAgo;
|
||||
const tooltipContent = heartbeatDate
|
||||
? `Last heartbeat: ${heartbeatDate.toLocaleString()}`
|
||||
: "No heartbeat data available";
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltipContent}>
|
||||
<span className={`cursor-default ${isHealthy ? "text-green-400" : "text-red-400"}`}>
|
||||
{isHealthy ? "Healthy" : "Unreachable"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const GatewayTab = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteGateway",
|
||||
"editDetails"
|
||||
] as const);
|
||||
|
||||
const deleteGatewayById = useDeleteGatewayById();
|
||||
const deleteGatewayV2ById = useDeleteGatewayV2ById();
|
||||
|
||||
const handleDeleteGateway = async () => {
|
||||
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
|
||||
if (data.isV1) {
|
||||
await deleteGatewayById.mutateAsync(data.id);
|
||||
} else {
|
||||
await deleteGatewayV2ById.mutateAsync(data.id);
|
||||
}
|
||||
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted gateway"
|
||||
});
|
||||
};
|
||||
|
||||
const filteredGateway = gateways?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Gateways</h3>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 py-0.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-400">
|
||||
Create and configure gateway to access private network resources from Infisical
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search gateway..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/2">Name</Th>
|
||||
<Th>
|
||||
Health Check
|
||||
<Tooltip
|
||||
asChild={false}
|
||||
className="normal-case"
|
||||
content="The last known healthcheck. Triggers every 1 hour."
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isGatewaysLoading && (
|
||||
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
|
||||
)}
|
||||
{filteredGateway?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Gateway v{el.isV1 ? "1" : "2"}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<GatewayHealthStatus heartbeat={el.heartbeat} />
|
||||
</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => navigator.clipboard.writeText(el.id)}
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
{el.isV1 && (
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.DeleteGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteGateway", el)}
|
||||
>
|
||||
Delete Gateway
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
<Modal
|
||||
isOpen={popUp.editDetails.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||
>
|
||||
<ModalContent title="Edit Gateway">
|
||||
<EditGatewayDetailsModal
|
||||
gatewayDetails={popUp.editDetails.data}
|
||||
onClose={() => handlePopUpToggle("editDetails")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isGatewaysLoading && !filteredGateway?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
gateways?.length
|
||||
? "No Gateways match search..."
|
||||
: "No Gateways have been configured"
|
||||
}
|
||||
icon={gateways?.length ? faSearch : faDoorClosed}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGateway.isOpen}
|
||||
title={`Are you sure you want to delete gateway ${
|
||||
(popUp?.deleteGateway?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteGateway()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { GatewayTab } from "./GatewayTab";
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { GatewayTab } from "../GatewayTab/GatewayTab";
|
||||
import { RelayTab } from "../RelayTab/RelayTab";
|
||||
|
||||
export const NetworkingTabGroup = () => {
|
||||
const search = useSearch({
|
||||
from: "/_authenticate/_inject-org-details/_org-layout/organization/networking/"
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ name: "Gateways", key: "gateways", component: GatewayTab },
|
||||
{ name: "Relays", key: "relays", component: RelayTab }
|
||||
];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
||||
|
||||
return (
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { NetworkingTabGroup } from "./NetworkingTabGroup";
|
||||
@@ -0,0 +1,198 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCopy,
|
||||
faDoorClosed,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionSubjects,
|
||||
OrgRelayPermissionActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
|
||||
|
||||
export const RelayTab = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const);
|
||||
|
||||
const deleteRelayById = useDeleteRelayById();
|
||||
|
||||
const handleDeleteRelay = async () => {
|
||||
const data = popUp.deleteRelay.data as { id: string };
|
||||
await deleteRelayById.mutateAsync(data.id);
|
||||
|
||||
handlePopUpToggle("deleteRelay");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted relay"
|
||||
});
|
||||
};
|
||||
|
||||
const filteredRelays = relays?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Relays</h3>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/relay-deployment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 py-0.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-400">
|
||||
Create and configure relays to securely access private network resources from Infisical
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search relay..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Host</Th>
|
||||
<Th>Created</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRelaysLoading && (
|
||||
<TableSkeleton innerKey="relay-table" columns={4} key="relay-table" />
|
||||
)}
|
||||
{filteredRelays?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
{!el.orgId && (
|
||||
<Tooltip content="This is a managed relay provided by Infisical">
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Managed
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{el.host}</Td>
|
||||
<Td>{formatRelative(new Date(el.createdAt), new Date())}</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => navigator.clipboard.writeText(el.id)}
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgRelayPermissionActions.DeleteRelays}
|
||||
a={OrgPermissionSubjects.Relay}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed || !el.orgId}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteRelay", el)}
|
||||
>
|
||||
Delete Relay
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isRelaysLoading && !filteredRelays?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
relays?.length ? "No Relays match search..." : "No Relays have been configured"
|
||||
}
|
||||
icon={relays?.length ? faSearch : faDoorClosed}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRelay.isOpen}
|
||||
title={`Are you sure you want to delete relay ${
|
||||
(popUp?.deleteRelay?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteRelay", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteRelay()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgRelayPermissionActions.ListRelays, subject: OrgPermissionSubjects.Relay }
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { RelayTab } from "./RelayTab";
|
||||
@@ -0,0 +1,3 @@
|
||||
export { GatewayTab } from "./GatewayTab/GatewayTab";
|
||||
export { NetworkingTabGroup } from "./NetworkingTabGroup/NetworkingTabGroup";
|
||||
export { RelayTab } from "./RelayTab/RelayTab";
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { NetworkingPage } from "./NetworkingPage";
|
||||
|
||||
const NetworkingPageQueryParams = z.object({
|
||||
selectedTab: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/networking/"
|
||||
)({
|
||||
component: NetworkingPage,
|
||||
validateSearch: zodValidator(NetworkingPageQueryParams),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Networking"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
+12
-1
@@ -11,7 +11,8 @@ import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionKmipActions,
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSecretShareAction
|
||||
OrgPermissionSecretShareAction,
|
||||
OrgRelayPermissionActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { TPermission } from "@app/hooks/api/roles/types";
|
||||
|
||||
@@ -90,6 +91,15 @@ const orgGatewayPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const orgRelayPermissionSchema = z
|
||||
.object({
|
||||
[OrgRelayPermissionActions.ListRelays]: z.boolean().optional(),
|
||||
[OrgRelayPermissionActions.EditRelays]: z.boolean().optional(),
|
||||
[OrgRelayPermissionActions.DeleteRelays]: z.boolean().optional(),
|
||||
[OrgRelayPermissionActions.CreateRelays]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
const machineIdentityAuthTemplatePermissionSchema = z
|
||||
.object({
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(),
|
||||
@@ -147,6 +157,7 @@ export const formSchema = z.object({
|
||||
"app-connections": appConnectionsPermissionSchema,
|
||||
kmip: kmipPermissionSchema,
|
||||
gateway: orgGatewayPermissionSchema,
|
||||
relay: orgRelayPermissionSchema,
|
||||
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
|
||||
"secret-share": secretSharingPermissionSchema
|
||||
})
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { OrgRelayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{ action: OrgRelayPermissionActions.ListRelays, label: "List Relays" },
|
||||
{ action: OrgRelayPermissionActions.CreateRelays, label: "Create Relays" },
|
||||
{ action: OrgRelayPermissionActions.EditRelays, label: "Edit Relays" },
|
||||
{ action: OrgRelayPermissionActions.DeleteRelays, label: "Delete Relays" }
|
||||
] as const;
|
||||
|
||||
export const OrgRelayPermissionRow = ({ isEditable, control, setValue }: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.relay"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSION_ACTIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
if (score === 1 && rule?.[OrgRelayPermissionActions.ListRelays]) return Permission.ReadOnly;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||
if (isRowCustom) {
|
||||
setIsRowExpanded.on();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
"permissions.relay",
|
||||
{
|
||||
[OrgRelayPermissionActions.ListRelays]: true,
|
||||
[OrgRelayPermissionActions.EditRelays]: true,
|
||||
[OrgRelayPermissionActions.CreateRelays]: true,
|
||||
[OrgRelayPermissionActions.DeleteRelays]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
"permissions.relay",
|
||||
{
|
||||
[OrgRelayPermissionActions.ListRelays]: true,
|
||||
[OrgRelayPermissionActions.EditRelays]: false,
|
||||
[OrgRelayPermissionActions.CreateRelays]: false,
|
||||
[OrgRelayPermissionActions.DeleteRelays]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
|
||||
case Permission.NoAccess:
|
||||
default:
|
||||
setValue(
|
||||
"permissions.relay",
|
||||
{
|
||||
[OrgRelayPermissionActions.ListRelays]: false,
|
||||
[OrgRelayPermissionActions.EditRelays]: false,
|
||||
[OrgRelayPermissionActions.CreateRelays]: false,
|
||||
[OrgRelayPermissionActions.DeleteRelays]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td className="w-4">
|
||||
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td className="w-full select-none">Relays</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="h-8 w-40 bg-mineshaft-700"
|
||||
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.relay.${action}`}
|
||||
key={`permissions.relay.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.relay.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -69,6 +69,7 @@ type Props = {
|
||||
| "organization-admin-console"
|
||||
| "kmip"
|
||||
| "gateway"
|
||||
| "relay"
|
||||
| "secret-share"
|
||||
| "billing"
|
||||
| "audit-logs"
|
||||
|
||||
+6
@@ -23,6 +23,7 @@ import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
|
||||
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
|
||||
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow";
|
||||
import { OrgRelayPermissionRow } from "./OrgPermissionRelayRow";
|
||||
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
@@ -188,6 +189,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgRelayPermissionRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionBillingRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
|
||||
@@ -349,9 +349,7 @@ export const OverviewPage = () => {
|
||||
getSecretRotationStatusesByName
|
||||
} = useSecretRotationOverview(secretRotations);
|
||||
|
||||
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(
|
||||
secrets?.concat(secretImportsShaped) || []
|
||||
);
|
||||
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(secrets || []);
|
||||
|
||||
const getSecretByKey = useCallback(
|
||||
(env: string, key: string) => {
|
||||
@@ -731,6 +729,9 @@ export const OverviewPage = () => {
|
||||
|
||||
userAvailableEnvs.forEach((env) => {
|
||||
secrets?.forEach((secret) => {
|
||||
// bulk actions don't apply to rotation secrets (move/delete)
|
||||
if (secret.isRotatedSecret) return;
|
||||
|
||||
if (allRowsSelectedOnPage.isChecked) {
|
||||
delete newChecks[EntryType.SECRET][secret.key];
|
||||
} else {
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetSecretValue } from "@app/hooks/api/dashboard/queries";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
interface SecretOverviewRotationSecretRowProps {
|
||||
secret: SecretV3RawSanitized | null;
|
||||
isSecretVisible: boolean;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
export const SecretOverviewRotationSecretRow = ({
|
||||
secret,
|
||||
isSecretVisible,
|
||||
environment,
|
||||
secretPath
|
||||
}: SecretOverviewRotationSecretRowProps) => {
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const canFetchValue = Boolean(secret);
|
||||
|
||||
const { data: secretValueData, isError } = useGetSecretValue(
|
||||
{
|
||||
secretKey: secret?.key ?? "",
|
||||
environment,
|
||||
secretPath,
|
||||
projectId: currentProject.id
|
||||
},
|
||||
{
|
||||
enabled: canFetchValue && (isSecretVisible || isFieldFocused)
|
||||
}
|
||||
);
|
||||
|
||||
const secretValue = isError
|
||||
? "Error loading secret value..."
|
||||
: (secretValueData?.valueOverride ?? secretValueData?.value ?? HIDDEN_SECRET_VALUE);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={secret ? undefined : "You do not have permission to view this secret."}
|
||||
>
|
||||
<tr className="!last:border-b-0 h-full hover:bg-mineshaft-700">
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>{secret?.key ?? "********"}</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
value={secretValue}
|
||||
isVisible={isSecretVisible}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
+9
-48
@@ -16,8 +16,6 @@ import { twMerge } from "tailwind-merge";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge";
|
||||
import { Badge, IconButton, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import {
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSub
|
||||
@@ -27,6 +25,8 @@ import { useToggle } from "@app/hooks";
|
||||
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
|
||||
|
||||
import { SecretOverviewRotationSecretRow } from "./SecretOverviewRotationSecretRow";
|
||||
|
||||
type Props = {
|
||||
secretRotationName: string;
|
||||
environments: { name: string; slug: string }[];
|
||||
@@ -256,52 +256,13 @@ export const SecretOverviewSecretRotationRow = ({
|
||||
<tbody className="border-t-2 border-mineshaft-600">
|
||||
{secrets.map((secret, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
secret
|
||||
? undefined
|
||||
: "You do not have permission to view this secret."
|
||||
}
|
||||
>
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
className="!last:border-b-0 h-full hover:bg-mineshaft-700"
|
||||
>
|
||||
<td
|
||||
className="flex h-full items-center"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
<span className={twMerge(!secret && "blur")}>
|
||||
{secret?.key ?? "********"}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="col-span-2 h-full w-full"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly
|
||||
value={secret.value}
|
||||
isVisible={isSecretVisible}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
<SecretOverviewRotationSecretRow
|
||||
key={`rotation-secret-${secretRotation.id}-${index + 1}`}
|
||||
secret={secret}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
isSecretVisible={isSecretVisible}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
+112
-21
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
@@ -10,14 +10,12 @@ import {
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
hasSecretReference,
|
||||
SecretReferenceTree
|
||||
} from "@app/components/secrets/SecretReferenceDetails";
|
||||
import { SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
IconButton,
|
||||
@@ -27,12 +25,23 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProject,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchSecretValue,
|
||||
useGetSecretValue
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { ProjectEnv, SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string | null;
|
||||
@@ -56,6 +65,15 @@ type Props = {
|
||||
) => Promise<void>;
|
||||
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
|
||||
isRotatedSecret?: boolean;
|
||||
isEmpty?: boolean;
|
||||
importedSecret?:
|
||||
| {
|
||||
secretPath: string;
|
||||
secret?: SecretV3RawSanitized;
|
||||
environmentInfo?: ProjectEnv;
|
||||
environment: string;
|
||||
}
|
||||
| undefined;
|
||||
importedBy?: {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
@@ -81,24 +99,68 @@ export const SecretEditRow = ({
|
||||
isVisible,
|
||||
secretId,
|
||||
isRotatedSecret,
|
||||
importedBy
|
||||
importedBy,
|
||||
importedSecret,
|
||||
isEmpty
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"editSecret"
|
||||
] as const);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
|
||||
const fetchSecretValueParams = importedSecret
|
||||
? {
|
||||
environment: importedSecret.environment,
|
||||
secretPath: importedSecret.secretPath,
|
||||
secretKey: importedSecret.secret?.key ?? "",
|
||||
projectId: currentProject.id
|
||||
}
|
||||
: {
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: secretName,
|
||||
projectId: currentProject.id,
|
||||
isOverride
|
||||
};
|
||||
|
||||
// scott: only fetch value if secret exists, has non-empty value and user has permission
|
||||
const canFetchValue = Boolean(importedSecret ?? secretId) && !isEmpty && !secretValueHidden;
|
||||
|
||||
const {
|
||||
data: secretValueData,
|
||||
isPending: isPendingSecretValueData,
|
||||
isError: isErrorFetchingSecretValue
|
||||
} = useGetSecretValue(fetchSecretValueParams, {
|
||||
enabled: canFetchValue && (isVisible || isFieldFocused)
|
||||
});
|
||||
|
||||
const isFetchingSecretValue = canFetchValue && isPendingSecretValueData;
|
||||
const isSecretValueFetched = Boolean(secretValueData);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm({
|
||||
values: {
|
||||
value: defaultValue || null
|
||||
defaultValues: {
|
||||
value: secretValueData?.valueOverride ?? secretValueData?.value ?? (defaultValue || null)
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (secretValueData && !isDirty) {
|
||||
setValue("value", secretValueData.valueOverride ?? secretValueData.value);
|
||||
}
|
||||
}, [secretValueData]);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
@@ -113,6 +175,25 @@ export const SecretEditRow = ({
|
||||
};
|
||||
|
||||
const handleCopySecretToClipboard = async () => {
|
||||
if (!isSecretValueFetched && !isDirty) {
|
||||
try {
|
||||
const data = await fetchSecretValue(fetchSecretValueParams);
|
||||
|
||||
queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data);
|
||||
|
||||
await window.navigator.clipboard.writeText(data.valueOverride ?? data.value);
|
||||
createNotification({ type: "success", text: "Copied secret to clipboard" });
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to fetch secret value."
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { value } = getValues();
|
||||
if (value) {
|
||||
try {
|
||||
@@ -221,14 +302,25 @@ export const SecretEditRow = ({
|
||||
)}
|
||||
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
|
||||
<Controller
|
||||
disabled={isImportedSecret && !defaultValue}
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
isReadOnly={isImportedSecret || (isRotatedSecret && !isOverride)}
|
||||
value={field.value as string}
|
||||
isReadOnly={
|
||||
isImportedSecret ||
|
||||
(isRotatedSecret && !isOverride) ||
|
||||
isFetchingSecretValue ||
|
||||
isErrorFetchingSecretValue
|
||||
}
|
||||
value={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
isFetchingSecretValue
|
||||
? HIDDEN_SECRET_VALUE
|
||||
: isErrorFetchingSecretValue
|
||||
? "Error fetching secret value..."
|
||||
: (field.value as string)
|
||||
}
|
||||
key="secret-input"
|
||||
isVisible={isVisible && !secretValueHidden}
|
||||
secretPath={secretPath}
|
||||
@@ -236,6 +328,11 @@ export const SecretEditRow = ({
|
||||
isImport={isImportedSecret}
|
||||
defaultValue={secretValueHidden ? "" : undefined}
|
||||
canEditButNotView={secretValueHidden && !isOverride}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
setIsFieldFocused.off();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -309,18 +406,12 @@ export const SecretEditRow = ({
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(defaultValue || "")
|
||||
? "Secret Reference Tree"
|
||||
: "Secret does not contain references"
|
||||
}
|
||||
>
|
||||
<Tooltip content="Secret Reference Tree">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="reference-tree"
|
||||
className="h-full"
|
||||
isDisabled={!hasSecretReference(defaultValue || "") || !canReadSecretValue}
|
||||
isDisabled={!canReadSecretValue || !secretId || isEmpty}
|
||||
>
|
||||
<FontAwesomeIcon icon={faProjectDiagram} />
|
||||
</IconButton>
|
||||
|
||||
+13
-4
@@ -50,7 +50,14 @@ type Props = {
|
||||
getImportedSecretByKey: (
|
||||
env: string,
|
||||
secretName: string
|
||||
) => { secret?: SecretV3RawSanitized; environmentInfo?: ProjectEnv } | undefined;
|
||||
) =>
|
||||
| {
|
||||
secret?: SecretV3RawSanitized;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
environmentInfo?: ProjectEnv;
|
||||
}
|
||||
| undefined;
|
||||
scrollOffset: number;
|
||||
importedBy?: {
|
||||
environment: { name: string; slug: string };
|
||||
@@ -140,7 +147,7 @@ export const SecretOverviewTableRow = ({
|
||||
const isSecretImported = isImportedSecretPresentInEnv(slug, secretKey);
|
||||
|
||||
const isSecretPresent = Boolean(secret);
|
||||
const isSecretEmpty = secret?.value === "";
|
||||
const isSecretEmpty = secret?.isEmpty;
|
||||
return (
|
||||
<Td
|
||||
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||
@@ -254,7 +261,7 @@ export const SecretOverviewTableRow = ({
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{secret?.valueOverride && (
|
||||
{secret?.idOverride && (
|
||||
<Tooltip content="Personal Override">
|
||||
<FontAwesomeIcon icon={faCodeBranch} />
|
||||
</Tooltip>
|
||||
@@ -266,11 +273,13 @@ export const SecretOverviewTableRow = ({
|
||||
secretPath={secretPath}
|
||||
isVisible={isSecretVisible}
|
||||
secretName={secretKey}
|
||||
isEmpty={secret?.isEmpty}
|
||||
secretValueHidden={secret?.secretValueHidden || false}
|
||||
defaultValue={getDefaultValue(secret, importedSecret)}
|
||||
secretId={secret?.id}
|
||||
isOverride={Boolean(secret?.valueOverride)}
|
||||
isOverride={Boolean(secret?.idOverride)}
|
||||
isImportedSecret={isImportedSecret}
|
||||
importedSecret={importedSecret}
|
||||
isCreatable={isCreatable}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onSecretCreate={onSecretCreate}
|
||||
|
||||
+30
-11
@@ -25,8 +25,10 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { reverseTruncate } from "@app/helpers/reverseTruncate";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { fetchSecretValue } from "@app/hooks/api/dashboard/queries";
|
||||
import { TDashboardProjectSecretsQuickSearch } from "@app/hooks/api/dashboard/types";
|
||||
import { ProjectEnv } from "@app/hooks/api/projects/types";
|
||||
|
||||
@@ -53,6 +55,8 @@ export const QuickSearchSecretItem = ({
|
||||
initialState: false
|
||||
});
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const [groupSecret] = secretGroup;
|
||||
|
||||
const handleNavigate = () => {
|
||||
@@ -67,14 +71,29 @@ export const QuickSearchSecretItem = ({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCopy = (value: string, env: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
createNotification({
|
||||
type: "info",
|
||||
title: isSingleEnv ? "Secret value copied." : `Secret value copied from ${env}.`,
|
||||
text: ""
|
||||
});
|
||||
setIsUrlCopied(true);
|
||||
const handleCopy = async (env: string) => {
|
||||
try {
|
||||
const data = await fetchSecretValue({
|
||||
environment: groupSecret.env,
|
||||
secretPath: groupSecret.path!,
|
||||
secretKey: groupSecret.key,
|
||||
projectId: currentProject.id
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(data.valueOverride ?? data.value!);
|
||||
createNotification({
|
||||
type: "info",
|
||||
title: isSingleEnv ? "Secret value copied." : `Secret value copied from ${env}.`,
|
||||
text: ""
|
||||
});
|
||||
setIsUrlCopied(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Error fetching secret value"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const secretGroupTags = secretGroup.flatMap((secret) => secret.tags);
|
||||
@@ -146,7 +165,7 @@ export const QuickSearchSecretItem = ({
|
||||
e.stopPropagation();
|
||||
const el = envSlugMap.get(groupSecret.env)?.name;
|
||||
if (el) {
|
||||
handleCopy(groupSecret.value!, el);
|
||||
handleCopy(el);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -173,7 +192,7 @@ export const QuickSearchSecretItem = ({
|
||||
e.stopPropagation();
|
||||
const el = envSlugMap.get(secret.env)?.name;
|
||||
if (el) {
|
||||
handleCopy(secret.value!, el);
|
||||
handleCopy(el);
|
||||
}
|
||||
}}
|
||||
key={secret.id}
|
||||
@@ -214,7 +233,7 @@ export const QuickSearchSecretItem = ({
|
||||
e.stopPropagation();
|
||||
const el = envSlugMap.get(secret.env)?.name;
|
||||
if (el) {
|
||||
handleCopy(secret.value!, el);
|
||||
handleCopy(el);
|
||||
}
|
||||
}}
|
||||
key={secret.id}
|
||||
|
||||
@@ -47,10 +47,12 @@ import {
|
||||
useGetWsTags
|
||||
} from "@app/hooks/api";
|
||||
import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { queryClient } from "@app/hooks/api/reactQuery";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
@@ -152,6 +154,10 @@ const Page = () => {
|
||||
}
|
||||
}, [isBatchMode, projectId, environment, secretPath, loadPendingChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) setIsVisible(false);
|
||||
}, [environment]);
|
||||
|
||||
const canReadSecret = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
@@ -183,17 +189,6 @@ const Page = () => {
|
||||
})
|
||||
);
|
||||
|
||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
}
|
||||
);
|
||||
|
||||
const canReadSecretImports = permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
@@ -271,7 +266,6 @@ const Page = () => {
|
||||
orderDirection,
|
||||
includeImports: canReadSecretImports && (isResourceTypeFiltered ? filter.include.import : true),
|
||||
includeFolders: isResourceTypeFiltered ? filter.include.folder : true,
|
||||
viewSecretValue: canReadSecretValue,
|
||||
includeDynamicSecrets:
|
||||
canReadDynamicSecret && (isResourceTypeFiltered ? filter.include.dynamic : true),
|
||||
includeSecrets: canReadSecret && (isResourceTypeFiltered ? filter.include.secret : true),
|
||||
@@ -347,6 +341,24 @@ const Page = () => {
|
||||
pendingChanges: changes,
|
||||
message
|
||||
});
|
||||
|
||||
if (!isProtectedBranch) {
|
||||
pendingChanges.secrets.forEach((secret) => {
|
||||
if (secret.type === "update" && secret.secretValue !== undefined) {
|
||||
queryClient.setQueryData(
|
||||
dashboardKeys.getSecretValue({
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: secret.newSecretName ?? secret.secretKey,
|
||||
isOverride: false
|
||||
}),
|
||||
{ value: secret.secretValue }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: isProtectedBranch
|
||||
? "Requested changes have been sent for review"
|
||||
@@ -570,6 +582,9 @@ const Page = () => {
|
||||
const newChecks = { ...selectedSecrets };
|
||||
|
||||
secrets?.forEach((secret) => {
|
||||
// bulk actions don't apply to rotation secrets (move/delete)
|
||||
if (secret.isRotatedSecret) return;
|
||||
|
||||
if (allRowsSelectedOnPage.isChecked) {
|
||||
delete newChecks[secret.id];
|
||||
} else {
|
||||
@@ -601,7 +616,9 @@ const Page = () => {
|
||||
return secrets;
|
||||
}
|
||||
|
||||
const mergedSecrets = [...(secrets || [])];
|
||||
const mergedSecrets = [...(secrets || [])] as (SecretV3RawSanitized & {
|
||||
originalKey?: string;
|
||||
})[];
|
||||
|
||||
pendingChanges.secrets.forEach((change) => {
|
||||
switch (change.type) {
|
||||
@@ -652,7 +669,8 @@ const Page = () => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
__v: 0
|
||||
})) || []
|
||||
: mergedSecrets[updateIndex].tags
|
||||
: mergedSecrets[updateIndex].tags,
|
||||
originalKey: mergedSecrets[updateIndex].key
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface PendingSecretDelete extends BasePendingChange {
|
||||
type: PendingAction.Delete;
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretValueHidden: boolean;
|
||||
}
|
||||
|
||||
// Folder-related change types
|
||||
@@ -408,7 +409,7 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
|
||||
const mergedUpdate: PendingSecretUpdate = {
|
||||
...existingUpdate,
|
||||
secretKey: existingUpdate.secretKey,
|
||||
originalValue: existingUpdate.originalValue,
|
||||
originalValue: change.originalValue,
|
||||
originalComment: existingUpdate.originalComment,
|
||||
originalSkipMultilineEncoding: existingUpdate.originalSkipMultilineEncoding,
|
||||
originalTags: existingUpdate.originalTags,
|
||||
|
||||
+168
-109
@@ -18,6 +18,7 @@ import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { useGetServerConfig } from "@app/hooks/api/admin";
|
||||
import {
|
||||
DynamicSecretAwsIamAuth,
|
||||
DynamicSecretAwsIamCredentialType,
|
||||
DynamicSecretProviders
|
||||
} from "@app/hooks/api/dynamicSecret/types";
|
||||
import { ProjectEnv } from "@app/hooks/api/types";
|
||||
@@ -28,6 +29,9 @@ const formSchema = z.object({
|
||||
provider: z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
|
||||
credentialType: z
|
||||
.nativeEnum(DynamicSecretAwsIamCredentialType)
|
||||
.default(DynamicSecretAwsIamCredentialType.IamUser),
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
@@ -47,6 +51,9 @@ const formSchema = z.object({
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
||||
credentialType: z
|
||||
.nativeEnum(DynamicSecretAwsIamCredentialType)
|
||||
.default(DynamicSecretAwsIamCredentialType.IamUser),
|
||||
roleArn: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
@@ -65,6 +72,9 @@ const formSchema = z.object({
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(DynamicSecretAwsIamAuth.IRSA),
|
||||
credentialType: z
|
||||
.nativeEnum(DynamicSecretAwsIamCredentialType)
|
||||
.default(DynamicSecretAwsIamCredentialType.IamUser),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
@@ -137,13 +147,15 @@ export const AwsIamInputForm = ({
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||
usernameTemplate: "{{randomUsername}}",
|
||||
provider: {
|
||||
method: DynamicSecretAwsIamAuth.AssumeRole
|
||||
method: DynamicSecretAwsIamAuth.AssumeRole,
|
||||
credentialType: DynamicSecretAwsIamCredentialType.IamUser
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
const method = watch("provider.method");
|
||||
const credentialType = watch("provider.credentialType");
|
||||
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
@@ -264,6 +276,39 @@ export const AwsIamInputForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="provider.credentialType"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Credential Type"
|
||||
>
|
||||
<>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
<SelectItem value={DynamicSecretAwsIamCredentialType.IamUser}>
|
||||
IAM User
|
||||
</SelectItem>
|
||||
<SelectItem value={DynamicSecretAwsIamCredentialType.TemporaryCredentials}>
|
||||
Temporary Credentials
|
||||
</SelectItem>
|
||||
</Select>
|
||||
<div className="mt-1 text-xs text-mineshaft-300">
|
||||
{value === DynamicSecretAwsIamCredentialType.IamUser
|
||||
? "Creates temporary IAM users with access keys"
|
||||
: "Uses STS to generate temporary credentials from your connection. Duration is controlled by the Default TTL setting above."}
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{method === DynamicSecretAwsIamAuth.AccessKey && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
@@ -318,22 +363,24 @@ export const AwsIamInputForm = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.awsPath"
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Path"
|
||||
className="flex-grow"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.awsPath"
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Path"
|
||||
className="flex-grow"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.region"
|
||||
@@ -341,7 +388,11 @@ export const AwsIamInputForm = ({
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS Region"
|
||||
className="flex-grow"
|
||||
className={
|
||||
credentialType === DynamicSecretAwsIamCredentialType.TemporaryCredentials
|
||||
? "w-full"
|
||||
: "flex-grow"
|
||||
}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
@@ -350,97 +401,105 @@ export const AwsIamInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.permissionBoundaryPolicyArn"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="IAM User Permission Boundary ARN"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="ARN to be attached to the generated user for AWS Permission Boundary."
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.userGroups"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Groups"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given groups."
|
||||
>
|
||||
<Input {...field} placeholder="group1,group2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.policyArns"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS Policy ARNs"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given policy arns."
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.policyDocument"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Policy Document"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will have the inline policy."
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<MetadataForm control={control} name="provider.tags" title="Tags" isValueRequired />
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.permissionBoundaryPolicyArn"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="IAM User Permission Boundary ARN"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="ARN to be attached to the generated user for AWS Permission Boundary."
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.userGroups"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Groups"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given groups."
|
||||
>
|
||||
<Input {...field} placeholder="group1,group2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.policyArns"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS Policy ARNs"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given policy arns."
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.policyDocument"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Policy Document"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will have the inline policy."
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
placeholder="{{randomUsername}}"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<MetadataForm control={control} name="provider.tags" title="Tags" isValueRequired />
|
||||
)}
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
+14
-3
@@ -13,6 +13,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Badge, Button, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { SecretVersionDiffView } from "@app/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView";
|
||||
import { HIDDEN_SECRET_VALUE_API_MASK } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
import {
|
||||
PendingChange,
|
||||
@@ -105,7 +106,12 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
version: 1, // placeholder, not used
|
||||
secretKey: change.newSecretName ? existingSecret.key : undefined,
|
||||
secretValue:
|
||||
change.secretValue !== undefined ? (existingSecret.value ?? "") : undefined,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
change.secretValue !== undefined
|
||||
? change.existingSecret.secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE_API_MASK
|
||||
: (change.originalValue ?? "")
|
||||
: undefined,
|
||||
tags: change.tags ? (existingSecret.tags?.map((tag) => tag.slug) ?? []) : undefined,
|
||||
secretMetadata: change.secretMetadata ? existingSecret.secretMetadata : undefined,
|
||||
skipMultilineEncoding:
|
||||
@@ -130,7 +136,7 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
}
|
||||
|
||||
if (change.type === PendingAction.Delete) {
|
||||
const { secretKey, secretValue } = change;
|
||||
const { secretKey, secretValue, secretValueHidden } = change;
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
@@ -143,7 +149,12 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
{
|
||||
version: 1, // placeholder, not used
|
||||
secretKey,
|
||||
secretValue
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
secretValue: secretValue
|
||||
? secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE_API_MASK
|
||||
: secretValue
|
||||
: undefined
|
||||
}
|
||||
]
|
||||
}}
|
||||
|
||||
+10
-8
@@ -154,20 +154,22 @@ const renderOutputForm = (
|
||||
}
|
||||
|
||||
if (provider === DynamicSecretProviders.AwsIam) {
|
||||
const { USERNAME, ACCESS_KEY, SECRET_ACCESS_KEY } = data as {
|
||||
const { USERNAME, ACCESS_KEY, SECRET_ACCESS_KEY, SESSION_TOKEN } = data as {
|
||||
ACCESS_KEY: string;
|
||||
SECRET_ACCESS_KEY: string;
|
||||
USERNAME: string;
|
||||
USERNAME?: string;
|
||||
SESSION_TOKEN?: string;
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<OutputDisplay label="AWS Username" value={USERNAME} />
|
||||
{USERNAME && <OutputDisplay label="AWS IAM Username" value={USERNAME} />}
|
||||
<OutputDisplay label="AWS IAM Access Key" value={ACCESS_KEY} />
|
||||
<OutputDisplay
|
||||
label="AWS IAM Secret Key"
|
||||
value={SECRET_ACCESS_KEY}
|
||||
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal."
|
||||
/>
|
||||
<OutputDisplay label="AWS IAM Secret Key" value={SECRET_ACCESS_KEY} />
|
||||
{SESSION_TOKEN && <OutputDisplay label="AWS IAM Session Token" value={SESSION_TOKEN} />}
|
||||
<div className="mt-2 text-xs text-mineshaft-300">
|
||||
Important: Copy these credentials now. You will not be able to see them again after you
|
||||
close the modal.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+167
-104
@@ -7,7 +7,11 @@ import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
|
||||
import { useGetServerConfig, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import {
|
||||
DynamicSecretAwsIamAuth,
|
||||
DynamicSecretAwsIamCredentialType,
|
||||
TDynamicSecret
|
||||
} from "@app/hooks/api/dynamicSecret/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { MetadataForm } from "../MetadataForm";
|
||||
@@ -16,6 +20,9 @@ const formSchema = z.object({
|
||||
inputs: z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
|
||||
credentialType: z
|
||||
.nativeEnum(DynamicSecretAwsIamCredentialType)
|
||||
.default(DynamicSecretAwsIamCredentialType.IamUser),
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
@@ -30,6 +37,9 @@ const formSchema = z.object({
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
||||
credentialType: z
|
||||
.nativeEnum(DynamicSecretAwsIamCredentialType)
|
||||
.default(DynamicSecretAwsIamCredentialType.IamUser),
|
||||
roleArn: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
@@ -43,6 +53,9 @@ const formSchema = z.object({
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(DynamicSecretAwsIamAuth.IRSA),
|
||||
credentialType: z
|
||||
.nativeEnum(DynamicSecretAwsIamCredentialType)
|
||||
.default(DynamicSecretAwsIamCredentialType.IamUser),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
@@ -78,6 +91,7 @@ const formSchema = z.object({
|
||||
newName: slugSchema().optional(),
|
||||
usernameTemplate: z.string().trim().nullable().optional()
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
@@ -115,6 +129,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
}
|
||||
});
|
||||
const method = watch("inputs.method");
|
||||
const credentialType = watch("inputs.credentialType");
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
@@ -235,6 +250,39 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="inputs.credentialType"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Credential Type"
|
||||
>
|
||||
<>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
<SelectItem value={DynamicSecretAwsIamCredentialType.IamUser}>
|
||||
IAM User
|
||||
</SelectItem>
|
||||
<SelectItem value={DynamicSecretAwsIamCredentialType.TemporaryCredentials}>
|
||||
Temporary Credentials
|
||||
</SelectItem>
|
||||
</Select>
|
||||
<div className="mt-1 text-xs text-mineshaft-300">
|
||||
{value === DynamicSecretAwsIamCredentialType.IamUser
|
||||
? "Creates temporary IAM users with access keys"
|
||||
: "Uses STS to generate temporary credentials from your connection. Duration is controlled by the Default TTL setting above."}
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{method === DynamicSecretAwsIamAuth.AccessKey && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
@@ -289,21 +337,24 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.awsPath"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Path"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.awsPath"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Path"
|
||||
className="flex-grow"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.region"
|
||||
@@ -311,7 +362,11 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS Region"
|
||||
className="flex-grow"
|
||||
className={
|
||||
credentialType === DynamicSecretAwsIamCredentialType.TemporaryCredentials
|
||||
? "w-full"
|
||||
: "flex-grow"
|
||||
}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
@@ -320,93 +375,101 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.userGroups"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Groups"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given groups."
|
||||
>
|
||||
<Input {...field} placeholder="group1,group2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.permissionBoundaryPolicyArn"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="IAM User Permission Boundary ARN"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="ARN to be attached to the generated user for AWS Permission Boundary."
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.policyArns"
|
||||
defaultValue="datacenter1"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS Policy ARNs"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given policy arns."
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.policyDocument"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Policy Document"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will have the inline policy."
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<MetadataForm control={control} name="inputs.tags" title="Tags" isValueRequired />
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.userGroups"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Groups"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given groups."
|
||||
>
|
||||
<Input {...field} placeholder="group1,group2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.permissionBoundaryPolicyArn"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="IAM User Permission Boundary ARN"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="ARN to be attached to the generated user for AWS Permission Boundary."
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.policyArns"
|
||||
defaultValue="datacenter1"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS Policy ARNs"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will get attached to given policy arns."
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.policyDocument"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="AWS IAM Policy Document"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="Generated users will have the inline policy."
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="usernameTemplate"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Username Template"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{credentialType !== DynamicSecretAwsIamCredentialType.TemporaryCredentials && (
|
||||
<MetadataForm control={control} name="inputs.tags" title="Tags" isValueRequired />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { faArrowRightArrowLeft, faEllipsisH, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEllipsisH, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
@@ -199,14 +199,15 @@ export const EnvironmentTabs = ({ secretPath }: Props) => {
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
)}
|
||||
{currentProject.environments.length > 1 && (
|
||||
{/* scott: removing until we have time to update for fetching secret value */}
|
||||
{/* {currentProject.environments.length > 1 && (
|
||||
<Tab className="ml-auto" value={COMPARE_ENVIRONMENT_TAB}>
|
||||
<div className="flex items-center gap-x-2 whitespace-nowrap">
|
||||
<FontAwesomeIcon icon={faArrowRightArrowLeft} />
|
||||
Compare Environments
|
||||
</div>
|
||||
</Tab>
|
||||
)}
|
||||
)} */}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<Modal
|
||||
|
||||
+12
-15
@@ -20,12 +20,14 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useResyncSecretReplication } from "@app/hooks/api";
|
||||
import { TSecretImport } from "@app/hooks/api/types";
|
||||
|
||||
import { SecretImportSecretRow } from "./SecretImportSecretRow";
|
||||
|
||||
type Props = {
|
||||
onDelete: () => void;
|
||||
environment: string;
|
||||
@@ -35,7 +37,10 @@ type Props = {
|
||||
importedSecrets: {
|
||||
key: string;
|
||||
value?: string;
|
||||
overriden: { env: string; secretPath: string };
|
||||
overridden: { env: string; secretPath: string };
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
isEmpty?: boolean;
|
||||
}[];
|
||||
searchTerm: string;
|
||||
onExpandReplicateSecrets: (id: string) => void;
|
||||
@@ -299,7 +304,6 @@ export const SecretImportItem = ({
|
||||
<tr>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
|
||||
{/* <td style={{ padding: "0.25rem 1rem" }}>Override</td> */}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -317,18 +321,11 @@ export const SecretImportItem = ({
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{filteredImportedSecrets.map(({ key, value }, index) => (
|
||||
<tr key={`${id}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput value={value} isReadOnly />
|
||||
</td>
|
||||
{/* <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td> */}
|
||||
</tr>
|
||||
{filteredImportedSecrets.map((secret, index) => (
|
||||
<SecretImportSecretRow
|
||||
secret={secret}
|
||||
key={`${id}-${secret.key}-${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+9
-3
@@ -64,18 +64,24 @@ export const computeImportedSecretRows = (
|
||||
const importedSecretEntries: {
|
||||
key: string;
|
||||
value?: string;
|
||||
overriden: {
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
overridden: {
|
||||
env: string;
|
||||
secretPath: string;
|
||||
};
|
||||
isEmpty?: boolean;
|
||||
}[] = [];
|
||||
|
||||
importedSec.secrets.forEach(({ key, value }) => {
|
||||
importedSec.secrets.forEach(({ key, value, env, path, isEmpty }) => {
|
||||
if (!importedEntry[key]) {
|
||||
importedSecretEntries.push({
|
||||
key,
|
||||
value,
|
||||
overriden: overridenSec?.[key]
|
||||
environment: env,
|
||||
secretPath: path,
|
||||
overridden: overridenSec?.[key],
|
||||
isEmpty
|
||||
});
|
||||
importedEntry[key] = true;
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { SecretInput } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetSecretValue } from "@app/hooks/api/dashboard/queries";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
type SecretImportSecretRowProps = {
|
||||
secret: {
|
||||
key: string;
|
||||
value?: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
isEmpty?: boolean;
|
||||
overridden: { env: string; secretPath: string };
|
||||
};
|
||||
};
|
||||
|
||||
export const SecretImportSecretRow = ({
|
||||
secret: { key, environment, secretPath = "/", isEmpty }
|
||||
}: SecretImportSecretRowProps) => {
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const canFetchSecretValue = !isEmpty;
|
||||
|
||||
const {
|
||||
data: secretValue,
|
||||
isPending: isPendingSecretValue,
|
||||
isError: isErrorFetchingSecretValue
|
||||
} = useGetSecretValue(
|
||||
{
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
projectId: currentProject.id
|
||||
},
|
||||
{
|
||||
enabled: isFieldFocused && canFetchSecretValue
|
||||
}
|
||||
);
|
||||
|
||||
const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValue;
|
||||
|
||||
const getValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (isErrorFetchingSecretValue) return "Error loading secret value";
|
||||
|
||||
return secretValue?.value || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="h-10 w-1/2" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10 w-1/2" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput
|
||||
value={getValue()}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
isReadOnly
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
+158
-269
@@ -1,36 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRotateRight,
|
||||
faCheckCircle,
|
||||
faCopy,
|
||||
faDesktop,
|
||||
faEyeSlash,
|
||||
faPlus,
|
||||
faProjectDiagram,
|
||||
faSearch,
|
||||
faServer,
|
||||
faShare,
|
||||
faTag,
|
||||
faTrash,
|
||||
faTriangleExclamation,
|
||||
faUser
|
||||
faTriangleExclamation
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
hasSecretReference,
|
||||
SecretReferenceTree
|
||||
} from "@app/components/secrets/SecretReferenceDetails";
|
||||
import { SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails";
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
@@ -60,9 +51,13 @@ import {
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getProjectBaseURL } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetSecretVersion } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchSecretValue,
|
||||
useGetSecretValue
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
@@ -70,6 +65,7 @@ import { camelCaseToSpaces } from "@app/lib/fn/string";
|
||||
|
||||
import { HIDDEN_SECRET_VALUE } from "./SecretItem";
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
import { SecretVersionItem } from "./SecretVersionItem";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -77,7 +73,7 @@ type Props = {
|
||||
secretPath: string;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
onClose: () => void;
|
||||
secret: SecretV3RawSanitized;
|
||||
secret: SecretV3RawSanitized & { originalKey?: string };
|
||||
onDeleteSecret: () => void;
|
||||
onSaveSecret: (
|
||||
orgSec: SecretV3RawSanitized,
|
||||
@@ -92,7 +88,7 @@ type Props = {
|
||||
export const SecretDetailSidebar = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
secret,
|
||||
secret: originalSecret,
|
||||
onDeleteSecret,
|
||||
onSaveSecret,
|
||||
tags,
|
||||
@@ -101,16 +97,90 @@ export const SecretDetailSidebar = ({
|
||||
secretPath,
|
||||
handleSecretShare
|
||||
}: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const canFetchSecretValue =
|
||||
Boolean(originalSecret) && !originalSecret.secretValueHidden && !originalSecret.isEmpty;
|
||||
|
||||
const fetchSecretValueParams = {
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: originalSecret?.originalKey || originalSecret?.key,
|
||||
projectId: currentProject.id,
|
||||
isOverride: Boolean(originalSecret?.idOverride)
|
||||
};
|
||||
|
||||
const {
|
||||
data: secretValueData,
|
||||
isPending: isPendingSecretValue,
|
||||
isError: isErrorFetchingSecretValue
|
||||
} = useGetSecretValue(fetchSecretValueParams, {
|
||||
enabled: canFetchSecretValue && isFieldFocused
|
||||
});
|
||||
|
||||
const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValue;
|
||||
const hasFetchedSecretValue = !canFetchSecretValue || Boolean(secretValueData);
|
||||
|
||||
const secret = {
|
||||
...originalSecret,
|
||||
value: originalSecret?.value ?? secretValueData?.value,
|
||||
valueOverride: originalSecret?.valueOverride ?? secretValueData?.valueOverride
|
||||
};
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const canEditSecretValue = permission.can(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secret.key,
|
||||
secretTags: ["*"]
|
||||
})
|
||||
);
|
||||
|
||||
const getDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (secret.secretValueHidden) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
|
||||
if (isErrorFetchingSecretValue) return "Error loading secret value...";
|
||||
|
||||
return secret.value || "";
|
||||
};
|
||||
|
||||
const getOverrideDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (secret.secretValueHidden) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
|
||||
if (isErrorFetchingSecretValue) return "Error loading secret value...";
|
||||
|
||||
return secret.valueOverride || "";
|
||||
};
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
getValues,
|
||||
getFieldState,
|
||||
formState: { isDirty }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: secret,
|
||||
values: {
|
||||
...secret,
|
||||
valueOverride: getOverrideDefaultValue(),
|
||||
value: getDefaultValue()
|
||||
},
|
||||
disabled: !secret
|
||||
});
|
||||
|
||||
@@ -119,9 +189,6 @@ export const SecretDetailSidebar = ({
|
||||
"secretReferenceTree"
|
||||
] as const);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const tagFields = useFieldArray({
|
||||
control,
|
||||
name: "tags"
|
||||
@@ -139,7 +206,6 @@ export const SecretDetailSidebar = ({
|
||||
{}
|
||||
);
|
||||
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const cannotEditSecret = permission.cannot(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
@@ -205,7 +271,16 @@ export const SecretDetailSidebar = ({
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||
await onSaveSecret(
|
||||
secret,
|
||||
{
|
||||
...secret,
|
||||
...data,
|
||||
value: getFieldState("value").isDirty ? data.value : undefined,
|
||||
valueOverride: getFieldState("valueOverride").isDirty ? data.valueOverride : undefined
|
||||
},
|
||||
() => reset()
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -218,58 +293,22 @@ export const SecretDetailSidebar = ({
|
||||
);
|
||||
}, [secret?.secretReminderRecipients]);
|
||||
|
||||
const getModifiedByIcon = (userType: string | undefined | null) => {
|
||||
switch (userType) {
|
||||
case ActorType.USER:
|
||||
return faUser;
|
||||
case ActorType.IDENTITY:
|
||||
return faDesktop;
|
||||
default:
|
||||
return faServer;
|
||||
}
|
||||
};
|
||||
const fetchValue = async () => {
|
||||
if (secretValueData) return secretValueData.valueOverride ?? secretValueData.value;
|
||||
|
||||
const getModifiedByName = (
|
||||
userType: string | undefined | null,
|
||||
userName: string | null | undefined
|
||||
) => {
|
||||
switch (userType) {
|
||||
case ActorType.PLATFORM:
|
||||
return "System-generated";
|
||||
case ActorType.IDENTITY:
|
||||
return userName || "Deleted Identity";
|
||||
case ActorType.USER:
|
||||
return userName || "Deleted User";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
try {
|
||||
const data = await fetchSecretValue(fetchSecretValueParams);
|
||||
|
||||
const getLinkToModifyHistoryEntity = (
|
||||
actorId: string,
|
||||
actorType: string,
|
||||
membershipId: string | null = ""
|
||||
) => {
|
||||
switch (actorType) {
|
||||
case ActorType.USER:
|
||||
return `/projects/secret-management/${currentProject.id}/members/${membershipId}`;
|
||||
case ActorType.IDENTITY:
|
||||
return `/projects/secret-management/${currentProject.id}/identities/${actorId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data);
|
||||
|
||||
const onModifyHistoryClick = (
|
||||
actorId: string | undefined | null,
|
||||
actorType: string | undefined | null,
|
||||
membershipId: string | undefined | null
|
||||
) => {
|
||||
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
|
||||
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
|
||||
if (redirectLink) {
|
||||
navigate({ to: redirectLink });
|
||||
}
|
||||
return data?.valueOverride ?? data.value;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Error fetching secret value"
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -313,6 +352,7 @@ export const SecretDetailSidebar = ({
|
||||
title={`Secret – ${secret?.key}`}
|
||||
className="thin-scrollbar h-full"
|
||||
cardBodyClassName="pb-0"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(handleFormSubmit)}
|
||||
@@ -356,7 +396,14 @@ export const SecretDetailSidebar = ({
|
||||
>
|
||||
<div className="flex items-start gap-x-2">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly || !isAllowed || secret?.isRotatedSecret}
|
||||
isReadOnly={
|
||||
isReadOnly ||
|
||||
!isAllowed ||
|
||||
secret?.isRotatedSecret ||
|
||||
isLoadingSecretValue ||
|
||||
isErrorFetchingSecretValue
|
||||
}
|
||||
canEditButNotView={secret.secretValueHidden && canEditSecretValue}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
key="secret-value"
|
||||
@@ -364,6 +411,11 @@ export const SecretDetailSidebar = ({
|
||||
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => {
|
||||
setIsFieldFocused.off();
|
||||
field.onBlur();
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
@@ -378,11 +430,17 @@ export const SecretDetailSidebar = ({
|
||||
className="px-2 py-[0.43rem] font-normal"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
||||
onClick={() => {
|
||||
const value = secret?.valueOverride ?? secret?.value;
|
||||
if (value) {
|
||||
handleSecretShare(value);
|
||||
onClick={async () => {
|
||||
let value: string | undefined;
|
||||
|
||||
if (hasFetchedSecretValue) {
|
||||
const values = getValues(["value", "valueOverride"]);
|
||||
value = secret.idOverride ? values[1] : values[0];
|
||||
} else {
|
||||
value = await fetchValue();
|
||||
}
|
||||
|
||||
handleSecretShare(value ?? "");
|
||||
}}
|
||||
>
|
||||
Share
|
||||
@@ -644,179 +702,21 @@ export const SecretDetailSidebar = ({
|
||||
<div className="dark flex max-h-[24rem] flex-1 cursor-default flex-col text-sm text-bunker-300">
|
||||
<div className="mb-0.5 text-mineshaft-400">Version History</div>
|
||||
<div className="thin-scrollbar flex flex-1 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(
|
||||
({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
|
||||
<div className="flex flex-row" key={id}>
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10">
|
||||
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="flex w-full cursor-default">
|
||||
<div className="relative w-10">
|
||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||
</div>
|
||||
<div className="flex w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
onClick={() =>
|
||||
onModifyHistoryClick(
|
||||
actor.actorId,
|
||||
actor.actorType,
|
||||
actor.membershipId
|
||||
)
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={getModifiedByIcon(actor.actorType)}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row">
|
||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||
Value:
|
||||
</div>
|
||||
<div className="group break-all pl-1 font-mono">
|
||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||
<button
|
||||
type="button"
|
||||
className="select-none text-left"
|
||||
onClick={(e) => {
|
||||
if (secretValueHidden) return;
|
||||
{secretVersion?.map((version) => (
|
||||
<SecretVersionItem
|
||||
canReadValue={!cannotReadSecretValue}
|
||||
secretVersion={version}
|
||||
secret={secret}
|
||||
currentVersion={secretVersion.length}
|
||||
onRevert={async (versionValue) => {
|
||||
await fetchValue();
|
||||
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
target.style.paddingBottom = "-1px";
|
||||
|
||||
// Create and insert popup
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||
popup.textContent = "Copied!";
|
||||
target.parentElement?.appendChild(popup);
|
||||
|
||||
// Remove popup and border after delay
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (secretValueHidden) return;
|
||||
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
target.style.paddingBottom = "-1px";
|
||||
|
||||
// Create and insert popup
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||
popup.textContent = "Copied!";
|
||||
target.parentElement?.appendChild(popup);
|
||||
|
||||
// Remove popup and border after delay
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
secretValueHidden && "text-xs text-bunker-300 opacity-40"
|
||||
)}
|
||||
>
|
||||
{secretValueHidden ? "Hidden" : secretValue}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE
|
||||
: secretValue?.replace(/./g, "*")}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.add("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.add("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!secret?.isRotatedSecret && (
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={() => setValue("value", secretValue, { shouldDirty: true })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
setTimeout(() => {
|
||||
setValue("value", versionValue, { shouldDirty: true });
|
||||
}, 5);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="dark flex flex-col text-sm text-bunker-300">
|
||||
@@ -942,26 +842,15 @@ export const SecretDetailSidebar = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-auto flex items-center space-x-2 pb-4">
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(secret?.value)
|
||||
? undefined
|
||||
: "Secret does not contain any references."
|
||||
}
|
||||
className="z-[100] text-center"
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="outline_bg"
|
||||
isDisabled={cannotReadSecretValue || secret.isEmpty}
|
||||
leftIcon={<FontAwesomeIcon icon={faProjectDiagram} />}
|
||||
onClick={() => handlePopUpOpen("secretReferenceTree", secretKey)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline_bg"
|
||||
isDisabled={cannotReadSecretValue || !hasSecretReference(secret?.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faProjectDiagram} />}
|
||||
onClick={() => handlePopUpOpen("secretReferenceTree", secretKey)}
|
||||
>
|
||||
Secret Reference Tree
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
Secret Reference Tree
|
||||
</Button>
|
||||
<Tooltip content="Copy Secret ID" className="z-[100]">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
|
||||
+135
-18
@@ -27,8 +27,8 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useProject
|
||||
useProject,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
@@ -47,6 +47,13 @@ import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { format } from "date-fns";
|
||||
import { CreateReminderForm } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchSecretValue,
|
||||
useGetSecretValue
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
@@ -56,11 +63,11 @@ import {
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
import { useBatchModeActions } from "../../SecretMainPage.store";
|
||||
|
||||
export const HIDDEN_SECRET_VALUE = "*****************************";
|
||||
export const HIDDEN_SECRET_VALUE = "*************************";
|
||||
export const HIDDEN_SECRET_VALUE_API_MASK = "<hidden-by-infisical>";
|
||||
|
||||
type Props = {
|
||||
secret: SecretV3RawSanitized;
|
||||
secret: SecretV3RawSanitized & { originalKey?: string };
|
||||
onSaveSecret: (
|
||||
orgSec: SecretV3RawSanitized,
|
||||
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
|
||||
@@ -91,7 +98,7 @@ type Props = {
|
||||
|
||||
export const SecretItem = memo(
|
||||
({
|
||||
secret,
|
||||
secret: originalSecret,
|
||||
onSaveSecret,
|
||||
onDeleteSecret,
|
||||
onDetailViewSecret,
|
||||
@@ -114,9 +121,43 @@ export const SecretItem = memo(
|
||||
] as const);
|
||||
const { currentProject } = useProject();
|
||||
const { permission } = useProjectPermission();
|
||||
const { isRotatedSecret } = secret;
|
||||
const { removePendingChange } = useBatchModeActions();
|
||||
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const canFetchSecretValue =
|
||||
!originalSecret.secretValueHidden &&
|
||||
!originalSecret.isEmpty &&
|
||||
pendingAction !== PendingAction.Create;
|
||||
|
||||
const fetchSecretValueParams = {
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: originalSecret.originalKey || originalSecret.key,
|
||||
projectId: currentProject.id,
|
||||
isOverride: Boolean(originalSecret.idOverride)
|
||||
};
|
||||
|
||||
const {
|
||||
data: secretValueData,
|
||||
isPending: isPendingSecretValueData,
|
||||
isError: isErrorFetchingSecretValue
|
||||
} = useGetSecretValue(fetchSecretValueParams, {
|
||||
enabled: canFetchSecretValue && (isVisible || isFieldFocused)
|
||||
});
|
||||
|
||||
const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValueData;
|
||||
const hasFetchedSecretValue = !canFetchSecretValue || Boolean(secretValueData);
|
||||
|
||||
const secret = {
|
||||
...originalSecret,
|
||||
value: originalSecret.value ?? secretValueData?.value,
|
||||
valueOverride: originalSecret.valueOverride ?? secretValueData?.valueOverride
|
||||
};
|
||||
|
||||
const { isRotatedSecret } = secret;
|
||||
|
||||
const autoSaveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const isAutoSavingRef = useRef(false);
|
||||
|
||||
@@ -139,10 +180,27 @@ export const SecretItem = memo(
|
||||
);
|
||||
|
||||
const getDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return undefined;
|
||||
|
||||
if (secret.secretValueHidden && !isPending) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
return secret.valueOverride || secret.value || "";
|
||||
|
||||
if (isErrorFetchingSecretValue) return undefined;
|
||||
|
||||
return secret.value || "";
|
||||
};
|
||||
|
||||
const getOverrideDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return undefined;
|
||||
|
||||
if (secret.secretValueHidden && !isPending) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
|
||||
if (isErrorFetchingSecretValue) return undefined;
|
||||
|
||||
return secret.valueOverride || "";
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -154,14 +212,17 @@ export const SecretItem = memo(
|
||||
reset,
|
||||
getValues,
|
||||
trigger,
|
||||
formState: { isDirty, isSubmitting, errors }
|
||||
formState: { isDirty, isSubmitting, errors },
|
||||
getFieldState
|
||||
} = useForm<TFormSchema>({
|
||||
defaultValues: {
|
||||
...secret,
|
||||
valueOverride: getOverrideDefaultValue(),
|
||||
value: getDefaultValue()
|
||||
},
|
||||
values: {
|
||||
...secret,
|
||||
valueOverride: getOverrideDefaultValue(),
|
||||
value: getDefaultValue()
|
||||
},
|
||||
resolver: zodResolver(formSchema)
|
||||
@@ -255,7 +316,10 @@ export const SecretItem = memo(
|
||||
);
|
||||
|
||||
const isReadOnlySecret =
|
||||
isReadOnly || isRotatedSecret || (isPending && pendingAction === PendingAction.Delete);
|
||||
isReadOnly ||
|
||||
isRotatedSecret ||
|
||||
(isPending && pendingAction === PendingAction.Delete) ||
|
||||
isLoadingSecretValue;
|
||||
|
||||
const { secretValueHidden } = secret;
|
||||
|
||||
@@ -322,12 +386,36 @@ export const SecretItem = memo(
|
||||
}
|
||||
};
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
const [overrideValue, value] = getValues(["value", "valueOverride"]);
|
||||
if (isOverridden) {
|
||||
navigator.clipboard.writeText(value as string);
|
||||
const fetchValue = async () => {
|
||||
if (secretValueData) return secretValueData;
|
||||
|
||||
try {
|
||||
const data = await fetchSecretValue(fetchSecretValueParams);
|
||||
|
||||
queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to fetch secret value"
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const copyTokenToClipboard = async () => {
|
||||
if (hasFetchedSecretValue) {
|
||||
const [overrideValue, value] = getValues(["value", "valueOverride"]);
|
||||
if (isOverridden) {
|
||||
navigator.clipboard.writeText(value as string);
|
||||
} else {
|
||||
navigator.clipboard.writeText(overrideValue as string);
|
||||
}
|
||||
} else {
|
||||
navigator.clipboard.writeText(overrideValue as string);
|
||||
const data = await fetchValue();
|
||||
navigator.clipboard.writeText((data.valueOverride ?? data.value) as string);
|
||||
}
|
||||
setIsSecValueCopied.on();
|
||||
};
|
||||
@@ -410,7 +498,7 @@ export const SecretItem = memo(
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{secretValueHidden && !isOverridden && !isPending && (
|
||||
{secretValueHidden && !getFieldState("value").isDirty && (
|
||||
<Tooltip
|
||||
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
|
||||
>
|
||||
@@ -424,10 +512,18 @@ export const SecretItem = memo(
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SecretInput
|
||||
isLoadingValue={isLoadingSecretValue && Boolean(secret.idOverride)}
|
||||
isErrorLoadingValue={isErrorFetchingSecretValue}
|
||||
key="value-overriden"
|
||||
isVisible={isVisible}
|
||||
isReadOnly={isReadOnly}
|
||||
{...field}
|
||||
onFocus={() => {
|
||||
if (secret.idOverride) setIsFieldFocused.on();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFieldFocused.off();
|
||||
field.onBlur();
|
||||
}}
|
||||
containerClassName="py-1.5 rounded-md transition-all"
|
||||
/>
|
||||
)}
|
||||
@@ -439,6 +535,8 @@ export const SecretItem = memo(
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
isLoadingValue={isLoadingSecretValue}
|
||||
isErrorLoadingValue={isErrorFetchingSecretValue}
|
||||
isReadOnly={isReadOnlySecret}
|
||||
key="secret-value"
|
||||
isVisible={isVisible && (!secretValueHidden || isPending)}
|
||||
@@ -446,6 +544,13 @@ export const SecretItem = memo(
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
onFocus={() => {
|
||||
setIsFieldFocused.on();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFieldFocused.off();
|
||||
field.onBlur();
|
||||
}}
|
||||
defaultValue={
|
||||
secretValueHidden && !isPending ? HIDDEN_SECRET_VALUE : undefined
|
||||
}
|
||||
@@ -613,7 +718,7 @@ export const SecretItem = memo(
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -682,7 +787,19 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="share-secret"
|
||||
onClick={() => onShareSecret(secret)}
|
||||
onClick={async () => {
|
||||
if (hasFetchedSecretValue) {
|
||||
onShareSecret(secret);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await fetchValue();
|
||||
|
||||
onShareSecret({
|
||||
...secret,
|
||||
...data
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip content="Share Secret">
|
||||
<FontAwesomeSymbol
|
||||
|
||||
+23
-15
@@ -466,7 +466,12 @@ export const SecretListView = ({
|
||||
);
|
||||
|
||||
const handleSecretDelete = useCallback(async () => {
|
||||
const { key, id: secretId, value } = popUp.deleteSecret?.data as SecretV3RawSanitized;
|
||||
const {
|
||||
key,
|
||||
id: secretId,
|
||||
value,
|
||||
secretValueHidden
|
||||
} = popUp.deleteSecret?.data as SecretV3RawSanitized;
|
||||
try {
|
||||
if (isBatchMode) {
|
||||
const deleteChange: PendingSecretDelete = {
|
||||
@@ -475,7 +480,8 @@ export const SecretListView = ({
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
timestamp: Date.now(),
|
||||
resourceType: "secret"
|
||||
resourceType: "secret",
|
||||
secretValueHidden
|
||||
};
|
||||
|
||||
addPendingChange(deleteChange, {
|
||||
@@ -616,19 +622,21 @@ export const SecretListView = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.secretDetail.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
|
||||
secret={popUp.secretDetail.data as SecretV3RawSanitized}
|
||||
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
|
||||
onClose={() => handlePopUpClose("secretDetail")}
|
||||
onSaveSecret={handleSaveSecret}
|
||||
tags={wsTags}
|
||||
onCreateTag={() => handlePopUpOpen("createTag")}
|
||||
handleSecretShare={(value: string) => handlePopUpOpen("createSharedSecret", { value })}
|
||||
/>
|
||||
{popUp.secretDetail.data && (
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.secretDetail.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
|
||||
secret={popUp.secretDetail.data as SecretV3RawSanitized}
|
||||
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
|
||||
onClose={() => handlePopUpClose("secretDetail")}
|
||||
onSaveSecret={handleSaveSecret}
|
||||
tags={wsTags}
|
||||
onCreateTag={() => handlePopUpOpen("createTag")}
|
||||
handleSecretShare={(value: string) => handlePopUpOpen("createSharedSecret", { value })}
|
||||
/>
|
||||
)}
|
||||
<CreateTagModal
|
||||
isOpen={popUp.createTag.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
|
||||
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
import { useState } from "react";
|
||||
import { faEye } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRotateRight,
|
||||
faDesktop,
|
||||
faEyeSlash,
|
||||
faServer,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { fetchSecretVersionValue } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretV3RawSanitized, SecretVersions } from "@app/hooks/api/secrets/types";
|
||||
|
||||
interface SecretVersionItemProps {
|
||||
secretVersion: SecretVersions;
|
||||
secret: SecretV3RawSanitized;
|
||||
currentVersion: number;
|
||||
onRevert: (secretValue: string) => void;
|
||||
canReadValue: boolean;
|
||||
}
|
||||
|
||||
export const SecretVersionItem = ({
|
||||
secretVersion: { createdAt, version, actor, secretValueHidden },
|
||||
secret,
|
||||
currentVersion,
|
||||
onRevert,
|
||||
canReadValue
|
||||
}: SecretVersionItemProps) => {
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getModifiedByIcon = (userType: string | undefined | null) => {
|
||||
switch (userType) {
|
||||
case ActorType.USER:
|
||||
return faUser;
|
||||
case ActorType.IDENTITY:
|
||||
return faDesktop;
|
||||
default:
|
||||
return faServer;
|
||||
}
|
||||
};
|
||||
|
||||
const getModifiedByName = (
|
||||
userType: string | undefined | null,
|
||||
userName: string | null | undefined
|
||||
) => {
|
||||
switch (userType) {
|
||||
case ActorType.PLATFORM:
|
||||
return "System-generated";
|
||||
case ActorType.IDENTITY:
|
||||
return userName || "Deleted Identity";
|
||||
case ActorType.USER:
|
||||
return userName || "Deleted User";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const getLinkToModifyHistoryEntity = (
|
||||
actorId: string,
|
||||
actorType: string,
|
||||
membershipId: string | null = ""
|
||||
) => {
|
||||
switch (actorType) {
|
||||
case ActorType.USER:
|
||||
return `/projects/secret-management/${currentProject.id}/members/${membershipId}`;
|
||||
case ActorType.IDENTITY:
|
||||
return `/projects/secret-management/${currentProject.id}/identities/${actorId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onModifyHistoryClick = (
|
||||
actorId: string | undefined | null,
|
||||
actorType: string | undefined | null,
|
||||
membershipId: string | undefined | null
|
||||
) => {
|
||||
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
|
||||
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
|
||||
if (redirectLink) {
|
||||
navigate({ to: redirectLink });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [secretValue, setSecretValue] = useState<string | null>(null);
|
||||
const [isFetchingValue, setIsFetchingValue] = useState(false);
|
||||
const handleGetSecretValue = async () => {
|
||||
if (secretValue) return secretValue;
|
||||
try {
|
||||
setIsFetchingValue(true);
|
||||
const value = await fetchSecretVersionValue(secret.id, version);
|
||||
setSecretValue(value);
|
||||
return value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to fetch secret version value"
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
setIsFetchingValue(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyValue = async (
|
||||
e: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
const value = await handleGetSecretValue();
|
||||
navigator.clipboard.writeText(value || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
target.style.paddingBottom = "-1px";
|
||||
|
||||
// Create and insert popup
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||
popup.textContent = "Copied!";
|
||||
target.parentElement?.appendChild(popup);
|
||||
|
||||
// Remove popup and border after delay
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10">
|
||||
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="flex w-full cursor-default">
|
||||
<div className="relative w-10">
|
||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||
</div>
|
||||
<div className="flex w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
onClick={() =>
|
||||
onModifyHistoryClick(actor.actorId, actor.actorType, actor.membershipId)
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row">
|
||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||
Value:
|
||||
</div>
|
||||
<div className="group break-all pl-1 font-mono">
|
||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||
<button
|
||||
type="button"
|
||||
className="select-none text-left"
|
||||
onClick={async (e) => {
|
||||
if (secretValueHidden) return;
|
||||
|
||||
await handleCopyValue(e);
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (secretValueHidden) return;
|
||||
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
await handleCopyValue(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={twMerge(secretValueHidden && "text-xs text-bunker-300 opacity-40")}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{secretValueHidden
|
||||
? "Hidden"
|
||||
: isFetchingValue
|
||||
? "****"
|
||||
: ((secretValue || <span className="text-mineshaft-400">EMPTY</span>) ??
|
||||
"Error fetching secret value...")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.currentTarget.closest(".group")?.classList.remove("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget.closest(".group")?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
****
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={async (e) => {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
await handleGetSecretValue();
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
await handleGetSecretValue();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!secret?.isRotatedSecret && canReadValue && (
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === currentVersion ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={async () => {
|
||||
if (secretValue) {
|
||||
onRevert(secretValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = await handleGetSecretValue();
|
||||
|
||||
onRevert(value);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+9
-39
@@ -15,13 +15,13 @@ import { twMerge } from "tailwind-merge";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge";
|
||||
import { IconButton, Modal, ModalContent, TableContainer, Tag, Tooltip } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretRotationSecretRow } from "./SecretRotationSecretRow";
|
||||
|
||||
type Props = {
|
||||
secretRotation: TSecretRotationV2;
|
||||
onEdit: () => void;
|
||||
@@ -209,43 +209,13 @@ export const SecretRotationItem = ({
|
||||
<tbody>
|
||||
{secrets.map((secret, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
secret ? undefined : "You do not have permission to view this secret."
|
||||
}
|
||||
>
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
className="h-full last:!border-b-0 hover:bg-mineshaft-700"
|
||||
>
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>
|
||||
{secret?.key ?? "********"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly
|
||||
value={secret.value}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
<SecretRotationSecretRow
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
secret={secret}
|
||||
environment={secretRotation.environment.slug}
|
||||
secretPath={secretRotation.folder.path}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetSecretValue } from "@app/hooks/api/dashboard/queries";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
interface SecretRotationSecretRowProps {
|
||||
secret: TSecretRotationV2["secrets"][number];
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
export const SecretRotationSecretRow = ({
|
||||
secret,
|
||||
environment,
|
||||
secretPath
|
||||
}: SecretRotationSecretRowProps) => {
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data: secretValue, isPending: isLoadingSecretValue } = useGetSecretValue(
|
||||
{
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: secret?.key ?? "",
|
||||
projectId: currentProject.id
|
||||
},
|
||||
{
|
||||
enabled: isFieldFocused && Boolean(secret)
|
||||
}
|
||||
);
|
||||
|
||||
const getValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (!secretValue) return "Error loading secret value";
|
||||
|
||||
return secretValue.value || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={secret ? undefined : "You do not have permission to view this secret."}
|
||||
>
|
||||
<tr className="h-full last:!border-b-0 hover:bg-mineshaft-700">
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>{secret?.key ?? "********"}</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
value={getValue()}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
+7
@@ -10,6 +10,7 @@ import { Button, FormControl, Input, Modal, ModalContent } from "@app/components
|
||||
import { useUser } from "@app/context";
|
||||
import { useRequestEmailChangeOTP, useUpdateUserEmail } from "@app/hooks/api/users";
|
||||
import { clearSession } from "@app/hooks/api/users/queries";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
|
||||
const emailSchema = z
|
||||
.object({
|
||||
@@ -43,6 +44,7 @@ const otpInputProps = {
|
||||
export const ChangeEmailSection = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUser();
|
||||
const hasEmailAuth = user?.authMethods?.includes(AuthMethod.EMAIL) ?? false;
|
||||
const [isOTPModalOpen, setIsOTPModalOpen] = useState(false);
|
||||
const [pendingEmail, setPendingEmail] = useState("");
|
||||
|
||||
@@ -177,6 +179,11 @@ export const ChangeEmailSection = () => {
|
||||
label="New email address"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
hasEmailAuth
|
||||
? "Your email authentication method is currently enabled and will remain active after changing your email."
|
||||
: "Email authentication method will be automatically enabled after changing your email. You may disable email authentication after logging in with your new email if needed."
|
||||
}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
|
||||
@@ -62,7 +62,7 @@ import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages
|
||||
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
|
||||
import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route'
|
||||
import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
|
||||
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
|
||||
import { Route as organizationNetworkingPageRouteImport } from './pages/organization/NetworkingPage/route'
|
||||
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
|
||||
import { Route as sshLayoutImport } from './pages/ssh/layout'
|
||||
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
|
||||
@@ -257,9 +257,9 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport =
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways',
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/networking',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
|
||||
createFileRoute(
|
||||
@@ -565,10 +565,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport.update({
|
||||
id: '/gateways',
|
||||
path: '/gateways',
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport.update({
|
||||
id: '/networking',
|
||||
path: '/networking',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
@@ -771,12 +771,12 @@ const organizationSecretSharingPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute,
|
||||
} as any)
|
||||
|
||||
const organizationGatewaysGatewayListPageRouteRoute =
|
||||
organizationGatewaysGatewayListPageRouteImport.update({
|
||||
const organizationNetworkingPageRouteRoute =
|
||||
organizationNetworkingPageRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute,
|
||||
} as any)
|
||||
|
||||
const organizationAppConnectionsAppConnectionsPageRouteRoute =
|
||||
@@ -2400,11 +2400,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
|
||||
path: '/gateways'
|
||||
fullPath: '/organization/gateways'
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/networking': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/networking'
|
||||
path: '/networking'
|
||||
fullPath: '/organization/networking'
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': {
|
||||
@@ -2435,12 +2435,12 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/networking/': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/networking/'
|
||||
path: '/'
|
||||
fullPath: '/organization/gateways/'
|
||||
preLoaderRoute: typeof organizationGatewaysGatewayListPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
|
||||
fullPath: '/organization/networking/'
|
||||
preLoaderRoute: typeof organizationNetworkingPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
|
||||
@@ -3771,19 +3771,18 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren {
|
||||
organizationGatewaysGatewayListPageRouteRoute: typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren {
|
||||
organizationNetworkingPageRouteRoute: typeof organizationNetworkingPageRouteRoute
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren =
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren =
|
||||
{
|
||||
organizationGatewaysGatewayListPageRouteRoute:
|
||||
organizationGatewaysGatewayListPageRouteRoute,
|
||||
organizationNetworkingPageRouteRoute: organizationNetworkingPageRouteRoute,
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute._addFileChildren(
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren,
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute._addFileChildren(
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren {
|
||||
@@ -3827,7 +3826,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
|
||||
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
|
||||
organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
|
||||
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
@@ -3845,8 +3844,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
|
||||
organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute:
|
||||
@@ -4766,12 +4765,12 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/environment': typeof adminEnvironmentPageRouteRoute
|
||||
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
|
||||
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
'/organization/networking': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren
|
||||
'/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
|
||||
'/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
|
||||
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
|
||||
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/organization/networking/': typeof organizationNetworkingPageRouteRoute
|
||||
'/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
|
||||
'/organization/settings/': typeof organizationSettingsPageRouteRoute
|
||||
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
@@ -4990,7 +4989,7 @@ export interface FileRoutesByTo {
|
||||
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
|
||||
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
|
||||
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/organization/networking': typeof organizationNetworkingPageRouteRoute
|
||||
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
|
||||
'/organization/settings': typeof organizationSettingsPageRouteRoute
|
||||
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
@@ -5211,12 +5210,12 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/networking': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/networking/': typeof organizationNetworkingPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
@@ -5445,12 +5444,12 @@ export interface FileRouteTypes {
|
||||
| '/admin/environment'
|
||||
| '/admin/integrations'
|
||||
| '/organization/app-connections'
|
||||
| '/organization/gateways'
|
||||
| '/organization/networking'
|
||||
| '/organization/secret-sharing'
|
||||
| '/organization/settings'
|
||||
| '/secret-manager/$projectId'
|
||||
| '/organization/app-connections/'
|
||||
| '/organization/gateways/'
|
||||
| '/organization/networking/'
|
||||
| '/organization/secret-sharing/'
|
||||
| '/organization/settings/'
|
||||
| '/organization/groups/$groupId'
|
||||
@@ -5668,7 +5667,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/integrations'
|
||||
| '/secret-manager/$projectId'
|
||||
| '/organization/app-connections'
|
||||
| '/organization/gateways'
|
||||
| '/organization/networking'
|
||||
| '/organization/secret-sharing'
|
||||
| '/organization/settings'
|
||||
| '/organization/groups/$groupId'
|
||||
@@ -5887,12 +5886,12 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/networking'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/settings'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/networking/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/settings/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
|
||||
@@ -6316,7 +6315,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/billing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/projects",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/networking",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
|
||||
@@ -6391,11 +6390,11 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways": {
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/networking": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/networking/"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing": {
|
||||
@@ -6425,9 +6424,9 @@ export const routeTree = rootRoute
|
||||
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/": {
|
||||
"filePath": "organization/Gateways/GatewayListPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/gateways"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/networking/": {
|
||||
"filePath": "organization/NetworkingPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/networking"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/": {
|
||||
"filePath": "organization/SecretSharingPage/route.tsx",
|
||||
|
||||
@@ -40,7 +40,7 @@ const organizationRoutes = route("/organization", [
|
||||
"organization/AppConnections/OauthCallbackPage/route.tsx"
|
||||
)
|
||||
]),
|
||||
route("/gateways", [index("organization/Gateways/GatewayListPage/route.tsx")])
|
||||
route("/networking", [index("organization/NetworkingPage/route.tsx")])
|
||||
]);
|
||||
|
||||
const secretManagerRoutes = route("/projects/secret-management/$projectId", [
|
||||
|
||||
Reference in New Issue
Block a user