Revert "Revert "feat(api/secrets): view secret value permission""

This commit is contained in:
Daniel Hougaard
2025-03-11 00:07:47 +04:00
committed by Daniel Hougaard
parent 3986df8e8a
commit 97567d06d4
70 changed files with 2105 additions and 729 deletions
@@ -13,12 +13,14 @@ import {
faShare,
faTag,
faTrash,
faUser
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 { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
@@ -48,6 +50,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
@@ -128,7 +131,7 @@ export const SecretDetailSidebar = ({
const navigate = useNavigate();
const cannotEditSecret = permission.cannot(
ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -136,16 +139,29 @@ export const SecretDetailSidebar = ({
secretTags: selectTagSlugs
})
);
const cannotReadSecretValue = permission.cannot(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
);
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
) && cannotEditSecret;
) &&
cannotEditSecret &&
cannotReadSecretValue;
const overrideAction = watch("overrideAction");
const isOverridden =
@@ -325,38 +341,63 @@ export const SecretDetailSidebar = ({
key="secret-value"
control={control}
render={({ field }) => (
<FormControl label="Value">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
<div className="flex items-center gap-2">
<FormControl
className="flex-1"
helperText={
cannotReadSecretValue ? (
<div className="flex space-x-2">
<FontAwesomeIcon
icon={faTriangleExclamation}
className="mt-0.5 text-yellow-400"
/>
<span>
The value of this secret is hidden because you do not have the
read secret value permission.
</span>
</div>
) : undefined
}
label="Value"
>
<div className="flex items-center gap-2">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
<Tooltip
content="You don't have permission to view the secret value."
isDisabled={!secret?.secretValueHidden}
>
<Button
isDisabled={secret?.secretValueHidden}
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);
}
}}
>
Share
</Button>
</Tooltip>
</div>
</FormControl>
</div>
)}
/>
)}
</ProjectPermissionCan>
</div>
<div className="ml-1 mt-1.5 flex items-center">
<Button
className="w-full 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);
}
}}
>
Share
</Button>
</div>
</div>
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div className="mb-4 px-4">
@@ -683,14 +724,17 @@ export const SecretDetailSidebar = ({
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<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, 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">
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
v{version}
{secretVersion?.map(
({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => (
<div key={`secret-version-${index + 1}`} 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">
<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>{format(new Date(createdAt), "Pp")}</div>
</div>
@@ -734,6 +778,8 @@ export const SecretDetailSidebar = ({
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";
@@ -753,6 +799,8 @@ export const SecretDetailSidebar = ({
}, 3000);
}}
onKeyDown={(e) => {
if (secretValueHidden) return;
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
@@ -774,7 +822,19 @@ export const SecretDetailSidebar = ({
}
}}
>
{secretValue}
<Tooltip
className="break-normal text-xs"
content="You do not have permission to view this secret value"
isDisabled={!secretValueHidden}
>
<span
className={twMerge(
secretValueHidden && "text-xs text-bunker-300 opacity-40"
)}
>
{secretValueHidden ? "Hidden" : secretValue}
</span>
</Tooltip>
</button>
<button
type="button"
@@ -798,7 +858,7 @@ export const SecretDetailSidebar = ({
</button>
</div>
<span className="group-[.show-value]:hidden">
{secretValue?.replace(/./g, "*")}
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"
@@ -822,24 +882,24 @@ export const SecretDetailSidebar = ({
</div>
</div>
</div>
<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)}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
</div>
</div>
<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)}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
</div>
</div>
))}
)
)}
</div>
</div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable simple-import-sort/imports */
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@@ -44,6 +45,8 @@ import {
SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import {
FontAwesomeSpriteName,
formSchema,
@@ -99,8 +102,14 @@ export const SecretItem = memo(
trigger,
formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({
defaultValues: secret,
values: secret,
defaultValues: {
...secret,
value: secret.secretValueHidden ? "" : secret.value
},
values: {
...secret,
value: secret.secretValueHidden ? "" : secret.value
},
resolver: zodResolver(formSchema)
});
@@ -123,7 +132,7 @@ export const SecretItem = memo(
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -132,7 +141,7 @@ export const SecretItem = memo(
})
) &&
permission.cannot(
ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -141,6 +150,8 @@ export const SecretItem = memo(
})
);
const { secretValueHidden } = secret;
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
useEffect(() => {
let timer: NodeJS.Timeout;
@@ -272,6 +283,8 @@ export const SecretItem = memo(
/>
)}
/>
) : secretValueHidden ? (
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : (
<Controller
name="value"
@@ -285,6 +298,7 @@ export const SecretItem = memo(
environment={environment}
secretPath={secretPath}
{...field}
defaultValue={secretValueHidden ? "" : undefined}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/>
)}
@@ -293,6 +307,7 @@ export const SecretItem = memo(
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
<Tooltip content="Copy secret">
<IconButton
isDisabled={secret.secretValueHidden}
ariaLabel="copy-value"
variant="plain"
size="sm"
@@ -500,6 +515,7 @@ export const SecretItem = memo(
)}
</ProjectPermissionCan>
<IconButton
isDisabled={secret.secretValueHidden}
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
variant="plain"
size="md"
@@ -1,4 +1,5 @@
import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { FontAwesomeSpriteName } from "./SecretListView.utils";
@@ -34,13 +35,7 @@ export const SecretNoAccessListView = ({ count }: Props) => {
className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/>
</div>
<div
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
<Blur />
</div>
</Tooltip>
))}