Merge pull request #3478 from Infisical/fix/UISecretEditPermissionButNotReadValuePermission

fix(secrets-table): UI fix for users with edit permissions but not read secret value permission
This commit is contained in:
carlosmonastyrski
2025-04-28 20:23:34 -03:00
committed by GitHub
6 changed files with 107 additions and 43 deletions
@@ -2,7 +2,7 @@ import path from "node:path";
import RE2 from "re2"; import RE2 from "re2";
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas"; import { SecretType, TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -720,7 +720,7 @@ export const reshapeBridgeSecret = (
secretReminderRecipients: secret.secretReminderRecipients || [], secretReminderRecipients: secret.secretReminderRecipients || [],
...(secretValueHidden ...(secretValueHidden
? { ? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, secretValue: secret.type === SecretType.Personal ? secret.value : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: true secretValueHidden: true
} }
: { : {
@@ -93,6 +93,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
onFocus={(evt) => { onFocus={(evt) => {
onFocus?.(evt); onFocus?.(evt);
setIsSecretFocused.on(); setIsSecretFocused.on();
evt.currentTarget.select();
}} }}
disabled={isDisabled} disabled={isDisabled}
spellCheck={false} spellCheck={false}
@@ -4,6 +4,7 @@ import { subject } from "@casl/ability";
import { import {
faCheck, faCheck,
faCopy, faCopy,
faEyeSlash,
faProjectDiagram, faProjectDiagram,
faTrash, faTrash,
faXmark faXmark
@@ -25,7 +26,6 @@ import {
ModalTrigger, ModalTrigger,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
@@ -124,7 +124,13 @@ export const SecretEditRow = ({
); );
} }
} }
reset({ value }); if (secretValueHidden && !isOverride) {
setTimeout(() => {
reset({ value: defaultValue || null });
}, 50);
} else {
reset({ value });
}
}; };
const canReadSecretValue = hasSecretReadValueOrDescribePermission( const canReadSecretValue = hasSecretReadValueOrDescribePermission(
@@ -132,6 +138,16 @@ export const SecretEditRow = ({
ProjectPermissionSecretActions.ReadValue ProjectPermissionSecretActions.ReadValue
); );
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: ["*"]
})
);
const handleDeleteSecret = useCallback(async () => { const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on(); setIsDeleting.on();
setIsModalOpen(false); setIsModalOpen(false);
@@ -153,29 +169,32 @@ export const SecretEditRow = ({
deleteKey={secretName} deleteKey={secretName}
onDeleteApproved={handleDeleteSecret} onDeleteApproved={handleDeleteSecret}
/> />
{secretValueHidden && !isOverride && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
<FontAwesomeIcon className="pl-2" size="sm" icon={faEyeSlash} />
</Tooltip>
)}
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2"> <div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
{secretValueHidden ? ( <Controller
<Blur tooltipText="You do not have permission to read the value of this secret." /> disabled={isImportedSecret && !defaultValue}
) : ( control={control}
<Controller name="value"
disabled={isImportedSecret && !defaultValue} render={({ field }) => (
control={control} <InfisicalSecretInput
name="value" {...field}
render={({ field }) => ( isReadOnly={isImportedSecret || (isRotatedSecret && !isOverride)}
<InfisicalSecretInput value={field.value as string}
{...field} key="secret-input"
isReadOnly={isImportedSecret || isRotatedSecret} isVisible={isVisible && !secretValueHidden}
value={field.value as string} secretPath={secretPath}
key="secret-input" environment={environment}
isVisible={isVisible} isImport={isImportedSecret}
secretPath={secretPath} defaultValue={secretValueHidden ? "" : undefined}
environment={environment} />
isImport={isImportedSecret} )}
/> />
)}
/>
)}
</div> </div>
<div <div
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faCircle } from "@fortawesome/free-regular-svg-icons"; import { faCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
@@ -14,6 +15,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2"; import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import {
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -64,6 +70,28 @@ export const SecretOverviewTableRow = ({
const totalCols = environments.length + 1; // secret key row const totalCols = environments.length + 1; // secret key row
const [isSecretVisible, setIsSecretVisible] = useToggle(); const [isSecretVisible, setIsSecretVisible] = useToggle();
const { permission } = useProjectPermission();
const getDefaultValue = (
secret: SecretV3RawSanitized | undefined,
importedSecret: { secret?: SecretV3RawSanitized } | undefined
) => {
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: secret?.env || "",
secretPath: secret?.path || "",
secretName: secret?.key || "",
secretTags: ["*"]
})
);
if (secret?.secretValueHidden && !secret?.valueOverride) {
return canEditSecretValue ? "******" : "";
}
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
};
return ( return (
<> <>
<Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group"> <Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group">
@@ -228,13 +256,7 @@ export const SecretOverviewTableRow = ({
isVisible={isSecretVisible} isVisible={isSecretVisible}
secretName={secretKey} secretName={secretKey}
secretValueHidden={secret?.secretValueHidden || false} secretValueHidden={secret?.secretValueHidden || false}
defaultValue={ defaultValue={getDefaultValue(secret, importedSecret)}
secret?.secretValueHidden
? ""
: secret?.valueOverride ||
secret?.value ||
importedSecret?.secret?.value
}
secretId={secret?.id} secretId={secret?.id}
isOverride={Boolean(secret?.valueOverride)} isOverride={Boolean(secret?.valueOverride)}
isImportedSecret={isImportedSecret} isImportedSecret={isImportedSecret}
@@ -46,10 +46,9 @@ import {
} from "@app/components/secrets/SecretReferenceDetails"; } from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faKey, faRotate } from "@fortawesome/free-solid-svg-icons"; import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
import { import {
FontAwesomeSpriteName, FontAwesomeSpriteName,
formSchema, formSchema,
@@ -57,6 +56,8 @@ import {
TFormSchema TFormSchema
} from "./SecretListView.utils"; } from "./SecretListView.utils";
const hiddenValue = "******";
type Props = { type Props = {
secret: SecretV3RawSanitized; secret: SecretV3RawSanitized;
onSaveSecret: ( onSaveSecret: (
@@ -95,6 +96,23 @@ export const SecretItem = memo(
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { isRotatedSecret } = secret; const { isRotatedSecret } = secret;
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: ["*"]
})
);
const getDefaultValue = () => {
if (secret.secretValueHidden) {
return canEditSecretValue ? hiddenValue : "";
}
return secret.valueOverride || secret.value || "";
};
const { const {
handleSubmit, handleSubmit,
control, control,
@@ -108,11 +126,11 @@ export const SecretItem = memo(
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: { defaultValues: {
...secret, ...secret,
value: secret.secretValueHidden ? "" : secret.value value: getDefaultValue()
}, },
values: { values: {
...secret, ...secret,
value: secret.secretValueHidden ? "" : secret.value value: getDefaultValue()
}, },
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
@@ -154,6 +172,7 @@ export const SecretItem = memo(
secretTags: selectedTagSlugs secretTags: selectedTagSlugs
}) })
); );
const { secretValueHidden } = secret; const { secretValueHidden } = secret;
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
@@ -286,6 +305,13 @@ export const SecretItem = memo(
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
{secretValueHidden && !isOverriden && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
<FontAwesomeIcon className="pr-2" size="sm" icon={faEyeSlash} />
</Tooltip>
)}
{isOverriden ? ( {isOverriden ? (
<Controller <Controller
name="valueOverride" name="valueOverride"
@@ -301,8 +327,6 @@ export const SecretItem = memo(
/> />
)} )}
/> />
) : secretValueHidden ? (
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : ( ) : (
<Controller <Controller
name="value" name="value"
@@ -312,11 +336,11 @@ export const SecretItem = memo(
<InfisicalSecretInput <InfisicalSecretInput
isReadOnly={isReadOnly || isRotatedSecret} isReadOnly={isReadOnly || isRotatedSecret}
key="secret-value" key="secret-value"
isVisible={isVisible} isVisible={isVisible && !secretValueHidden}
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}
{...field} {...field}
defaultValue={secretValueHidden ? "" : undefined} defaultValue={secretValueHidden ? hiddenValue : undefined}
containerClassName="py-1.5 rounded-md transition-all" containerClassName="py-1.5 rounded-md transition-all"
/> />
)} )}
@@ -49,8 +49,6 @@ export const SecretListView = ({
isProtectedBranch = false, isProtectedBranch = false,
importedBy importedBy
}: Props) => { }: Props) => {
console.log("secretssssss", secrets);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteSecret", "deleteSecret",