Merge remote-tracking branch 'origin/main' into feat/pki-ENG-3666

This commit is contained in:
Carlos Monastyrski
2025-09-22 15:52:41 -03:00
128 changed files with 5873 additions and 2385 deletions
@@ -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}`}>
&#36;&#123;<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#36;&#123;
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#125;
</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,
+17 -1
View File
@@ -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 } = {
+17 -1
View File
@@ -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"
}
+65 -11
View File
@@ -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
});
};
+18 -1
View File
@@ -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;
+3
View File
@@ -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() });
}
});
};
+21
View File
@@ -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
});
};
+13
View File
@@ -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 = {
+38 -6
View File
@@ -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),
+11
View File
@@ -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";
@@ -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"
}
]
})
});
@@ -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
})
@@ -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>
)}
</>
);
};
@@ -69,6 +69,7 @@ type Props = {
| "organization-admin-console"
| "kmip"
| "gateway"
| "relay"
| "secret-share"
| "billing"
| "audit-logs"
@@ -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 {
@@ -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>
);
};
@@ -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>
@@ -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>
@@ -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}
@@ -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,
@@ -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}
@@ -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
}
]
}}
@@ -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>
);
}
@@ -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">
@@ -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
@@ -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>
@@ -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;
}
@@ -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>
);
};
@@ -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"
@@ -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
@@ -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)}
@@ -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>
);
};
@@ -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>
@@ -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>
);
};
@@ -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}
+46 -47
View File
@@ -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",
+1 -1
View File
@@ -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", [