requested changes

This commit is contained in:
Daniel Hougaard
2025-03-11 04:52:12 +04:00
parent b9b76579ac
commit 483fb458dd
12 changed files with 441 additions and 191 deletions

View File

@@ -23,12 +23,6 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
subjectFields?: SecretSubjectFields
) {
try {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
} else {
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
}
} catch {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
@@ -40,6 +34,12 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
ProjectPermissionSub.Secrets
);
}
} catch {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
} else {
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
}
}
}

View File

@@ -834,6 +834,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/accessible-secrets",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
filterByAction: z
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
.default(ProjectPermissionSecretActions.ReadValue)
}),
response: {
200: z.object({
secrets: secretRawSchema
.extend({
secretPath: z.string().optional()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId, environment, secretPath, filterByAction } = req.query;
const { secrets } = await server.services.secret.getAccessibleSecrets({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment,
secretPath,
projectId,
filterByAction
});
return { secrets };
}
});
server.route({
method: "GET",
url: "/secrets-by-keys",

View File

@@ -60,6 +60,7 @@ import {
TCreateSecretDTO,
TDeleteManySecretDTO,
TDeleteSecretDTO,
TGetAccessibleSecretsDTO,
TGetASecretDTO,
TGetSecretReferencesTreeDTO,
TGetSecretsDTO,
@@ -200,7 +201,7 @@ export const secretV2BridgeServiceFactory = ({
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
references.forEach((el) => {
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: el.environment,
secretPath: el.secretPath,
secretName: el.secretKey,
@@ -1291,7 +1292,7 @@ export const secretV2BridgeServiceFactory = ({
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: expandEnvironment,
@@ -1347,7 +1348,7 @@ export const secretV2BridgeServiceFactory = ({
let secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
if (shouldExpandSecretReferences && secretValue) {
if (shouldExpandSecretReferences && secretValue && viewSecretValue) {
// eslint-disable-next-line
const expandedSecretValue = await expandSecretReferences({
environment,
@@ -2613,6 +2614,86 @@ export const secretV2BridgeServiceFactory = ({
return { tree: stackTrace, value: expandedValue };
};
const getAccessibleSecrets = async ({
projectId,
secretPath,
environment,
filterByAction,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TGetAccessibleSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath
});
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return { secrets: [] };
const secrets = await secretDAL.findByFolderIds([folder.id]);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedSecrets = secrets
.filter((el) => {
if (
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
) {
return false;
}
if (filterByAction === ProjectPermissionSecretActions.ReadValue) {
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
});
}
return true;
})
.map((secret) => {
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
},
false
);
});
return {
secrets: decryptedSecrets
};
};
return {
createSecret,
deleteSecret,
@@ -2630,6 +2711,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsMultiEnv,
getSecretReferenceTree,
getSecretsByFolderMappings,
getSecretById
getSecretById,
getAccessibleSecrets
};
};

View File

@@ -351,3 +351,10 @@ export type TGetSecretsRawByFolderMappingsDTO = {
filters: TFindSecretsByFolderIdsFilter;
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
};
export type TGetAccessibleSecretsDTO = {
environment: string;
projectId: string;
secretPath: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;

View File

@@ -81,6 +81,7 @@ import {
TDeleteManySecretRawDTO,
TDeleteSecretDTO,
TDeleteSecretRawDTO,
TGetAccessibleSecretsDTO,
TGetASecretByIdRawDTO,
TGetASecretDTO,
TGetASecretRawDTO,
@@ -1312,6 +1313,39 @@ export const secretServiceFactory = ({
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
};
const getAccessibleSecrets = async ({
projectId,
secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge) {
throw new BadRequestError({
message: "Project version does not support this endpoint.",
name: "ProjectVersionNotSupported"
});
}
const secrets = await secretV2BridgeService.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction,
actor,
actorId,
actorOrgId,
actorAuthMethod
});
return secrets;
};
const getSecretsRaw = async ({
projectId,
path,
@@ -3261,6 +3295,7 @@ export const secretServiceFactory = ({
getSecretReferenceTree,
getSecretsRawByFolderMappings,
getSecretAccessList,
getSecretByIdRaw
getSecretByIdRaw,
getAccessibleSecrets
};
};

View File

@@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
@@ -180,6 +181,12 @@ export enum SecretsOrderBy {
Name = "name" // "key" for secrets but using name for use across resources
}
export type TGetAccessibleSecretsDTO = {
secretPath: string;
environment: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean;
path: string;

View File

@@ -1,4 +1,5 @@
export {
useGetAccessibleSecrets,
useGetProjectSecretsDetails,
useGetProjectSecretsOverview,
useGetProjectSecretsQuickSearch

View File

@@ -11,6 +11,7 @@ import {
DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse,
TGetAccessibleSecretsDTO,
TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO,
@@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { groupBy, unique } from "@app/lib/fn/array";
import { SecretV3Raw } from "../types";
export const dashboardKeys = {
all: () => ["dashboard"] as const,
getDashboardSecrets: ({
@@ -58,7 +61,14 @@ export const dashboardKeys = {
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
"quick-search",
params
] as const
] as const,
getAccessibleSecrets: ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) =>
[...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const
};
export const fetchProjectSecretsOverview = async ({
@@ -295,6 +305,22 @@ export const fetchProjectSecretsQuickSearch = async ({
return data;
};
const fetchAccessibleSecrets = async ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>(
"/api/v1/dashboard/accessible-secrets",
{
params: { projectId, secretPath, environment, filterByAction }
}
);
return data.secrets;
};
export const useGetProjectSecretsQuickSearch = (
{
projectId,
@@ -357,3 +383,32 @@ export const useGetProjectSecretsQuickSearch = (
placeholderData: (previousData) => previousData
});
};
export const useGetAccessibleSecrets = ({
projectId,
secretPath,
environment,
filterByAction,
options
}: TGetAccessibleSecretsDTO & {
options?: Omit<
UseQueryOptions<
SecretV3Raw[],
unknown,
SecretV3Raw[],
ReturnType<typeof dashboardKeys.getAccessibleSecrets>
>,
"queryKey" | "queryFn"
>;
}) => {
return useQuery({
...options,
queryKey: dashboardKeys.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction
}),
queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction })
});
};

View File

@@ -1,3 +1,4 @@
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -101,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = {
environment: string;
keys: string[];
};
export type TGetAccessibleSecretsDTO = {
projectId: string;
secretPath: string;
environment: string;
filterByAction:
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;
};

View File

@@ -27,9 +27,11 @@ import {
} from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { SecretType } from "@app/hooks/api/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
type Props = {
defaultValue?: string | null;
@@ -79,6 +81,9 @@ export const SecretEditRow = ({
value: defaultValue || null
}
});
const { permission } = useProjectPermission();
const [isDeleting, setIsDeleting] = useToggle();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
@@ -120,6 +125,11 @@ export const SecretEditRow = ({
reset({ value });
};
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue
);
const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on();
setIsModalOpen(false);
@@ -229,48 +239,43 @@ export const SecretEditRow = ({
</IconButton>
</Tooltip>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Modal>
<ModalTrigger asChild>
<div className="opacity-0 group-hover:opacity-100">
<Tooltip
content={
hasSecretReference(defaultValue || "")
? "Secret Reference Tree"
: "Secret does not contain references"
}
>
<IconButton
variant="plain"
ariaLabel="reference-tree"
className="h-full"
isDisabled={!hasSecretReference(defaultValue || "") || !isAllowed}
>
<FontAwesomeIcon icon={faProjectDiagram} />
</IconButton>
</Tooltip>
</div>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
<div className="opacity-0 group-hover:opacity-100">
<Modal>
<ModalTrigger asChild>
<div className="opacity-0 group-hover:opacity-100">
<Tooltip
content={
hasSecretReference(defaultValue || "")
? "Secret Reference Tree"
: "Secret does not contain references"
}
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={secretName}
/>
</ModalContent>
</Modal>
</div>
)}
</ProjectPermissionCan>
<IconButton
variant="plain"
ariaLabel="reference-tree"
className="h-full"
isDisabled={!hasSecretReference(defaultValue || "") || !canReadSecretValue}
>
<FontAwesomeIcon icon={faProjectDiagram} />
</IconButton>
</Tooltip>
</div>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={secretName}
/>
</ModalContent>
</Modal>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, {

View File

@@ -20,8 +20,9 @@ import {
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
import { useGetAccessibleSecrets } from "@app/hooks/api/dashboard";
const formSchema = z.object({
environment: z.object({ name: z.string(), slug: z.string() }),
@@ -32,7 +33,7 @@ const formSchema = z.object({
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
),
secrets: z
.object({ key: z.string(), value: z.string().optional() })
.object({ secretKey: z.string(), secretValue: z.string().optional() })
.array()
.min(1, "Select one or more secrets to copy")
});
@@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({
const selectedEnvSlug = watch("environment");
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
environment: selectedEnvSlug.slug,
secretPath: debouncedEnvCopySecretPath,
options: {
enabled:
Boolean(workspaceId) &&
Boolean(selectedEnvSlug) &&
Boolean(debouncedEnvCopySecretPath) &&
isOpen
}
});
const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } =
useGetAccessibleSecrets({
projectId: workspaceId,
secretPath: debouncedEnvCopySecretPath,
environment: selectedEnvSlug.slug,
filterByAction: shouldIncludeValues
? ProjectPermissionSecretActions.ReadValue
: ProjectPermissionSecretActions.DescribeSecret,
options: {
enabled:
Boolean(workspaceId) &&
Boolean(selectedEnvSlug) &&
Boolean(debouncedEnvCopySecretPath) &&
isOpen
}
});
useEffect(() => {
setValue("secrets", []);
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
const handleSecSelectAll = () => {
if (secrets) {
setValue("secrets", secrets, { shouldDirty: true });
if (accessibleSecrets) {
setValue("secrets", accessibleSecrets, { shouldDirty: true });
}
};
const handleFormSubmit = async (data: TFormSchema) => {
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
data.secrets.forEach(({ key, value }) => {
secretsToBePulled[key] = {
value: (shouldIncludeValues && value) || "",
data.secrets.forEach(({ secretKey, secretValue }) => {
secretsToBePulled[secretKey] = {
value: (shouldIncludeValues && secretValue) || "",
comments: [""]
};
});
@@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({
<FilterableSelect
placeholder={
// eslint-disable-next-line no-nested-ternary
isSecretsLoading
isAccessibleSecretsLoading
? "Loading secrets..."
: secrets?.length
: accessibleSecrets?.length
? "Select secrets..."
: "No secrets found..."
}
isLoading={isSecretsLoading}
options={secrets}
isLoading={isAccessibleSecretsLoading}
options={accessibleSecrets}
value={value}
onChange={onChange}
isMulti
getOptionValue={(option) => option.key}
getOptionLabel={(option) => option.key}
getOptionValue={(option) => option.secretKey}
getOptionLabel={(option) => option.secretKey}
/>
</FormControl>
)}
@@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({
<Switch
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)}
onCheckedChange={(isChecked) => {
setValue("secrets", []);
setShouldIncludeValues(isChecked as boolean);
}}
>
Include secret values
</Switch>

View File

@@ -13,8 +13,8 @@ import {
faShare,
faTag,
faTrash,
faUser,
faTriangleExclamation
faTriangleExclamation,
faUser
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -728,8 +728,8 @@ export const SecretDetailSidebar = ({
<div className="mb-2 pl-1">Version History</div>
<div className="thin-scrollbar flex h-48 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, version, id, secretValueHidden, actor }, index) => (
<div key={`secret-version-${index + 1}`} className="flex flex-row">
({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
<div className="flex flex-row">
<div key={id} className="flex w-full flex-col space-y-1">
<div className="flex items-center">
<div className="w-10">
@@ -739,72 +739,50 @@ export const SecretDetailSidebar = ({
</div>
<div>{format(new Date(createdAt), "Pp")}</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"
<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)}
>
<FontAwesomeIcon
icon={getModifiedByIcon(actor.actorType)}
className="ml-2"
/>
</div>
</Tooltip>
{/* 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>
)}
<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;
)}
<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;
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";
@@ -822,13 +800,30 @@ export const SecretDetailSidebar = ({
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
<Tooltip
className="break-normal text-xs"
content="You do not have permission to view this secret value"
isDisabled={!secretValueHidden}
}}
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(
@@ -837,50 +832,50 @@ export const SecretDetailSidebar = ({
>
{secretValueHidden ? "Hidden" : secretValue}
</span>
</Tooltip>
</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 === " ") {
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}
}}
>
<FontAwesomeIcon icon={faEyeSlash} />
</button>
</div>
<span className="group-[.show-value]:hidden">
{secretValueHidden ? "******" : 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 === " ") {
}}
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 ? "******" : secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget
.closest(".group")
?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
}}
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>