mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 22:25:44 +00:00
Revert "Revert "feat(api/secrets): view secret value permission""
This commit is contained in:
committed by
Daniel Hougaard
parent
3986df8e8a
commit
97567d06d4
@@ -1,13 +1,21 @@
|
||||
import { useState } from "react";
|
||||
import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
faChevronRight,
|
||||
faExclamationTriangle,
|
||||
faEye,
|
||||
faEyeSlash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { AxiosError } from "axios";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetSecretReferenceTree } from "@app/hooks/api";
|
||||
import { TSecretReferenceTraceNode } from "@app/hooks/api/types";
|
||||
import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types";
|
||||
|
||||
import style from "./SecretReferenceDetails.module.css";
|
||||
|
||||
@@ -84,7 +92,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
|
||||
const { data, isPending } = useGetSecretReferenceTree({
|
||||
const { data, isPending, isError, error } = useGetSecretReferenceTree({
|
||||
secretPath,
|
||||
environmentSlug: environment,
|
||||
projectId,
|
||||
@@ -94,6 +102,26 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
const tree = data?.tree;
|
||||
const secretValue = data?.value;
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof AxiosError) {
|
||||
const err = error?.response?.data as TApiErrors;
|
||||
|
||||
if (err?.error === ApiErrorTypes.CustomForbiddenError) {
|
||||
createNotification({
|
||||
title: "You don't have permission to view reference tree",
|
||||
text: "You don't have permission to view one or more of the referenced secrets.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
createNotification({
|
||||
title: "Error fetching secret reference tree",
|
||||
text: "Please try again later.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
@@ -114,11 +142,16 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
</FormControl>
|
||||
<FormLabel className="mb-2" label="Reference Tree" />
|
||||
<div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200">
|
||||
{tree && (
|
||||
{isError ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="mr-2 text-red-500" />
|
||||
<p className="text-red-500">Error fetching secret reference tree</p>
|
||||
</div>
|
||||
) : tree ? (
|
||||
<ul className={style.tree}>
|
||||
<SecretReferenceNode node={tree} isRoot secretKey={secretKey} />
|
||||
</ul>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-mineshaft-400">
|
||||
Click a secret key to view its sub-references.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Tooltip } from "../Tooltip/Tooltip";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
tooltipText?: string;
|
||||
}
|
||||
|
||||
export const Blur = ({ className, tooltipText }: IProps) => {
|
||||
return (
|
||||
<Tooltip content={tooltipText} isDisabled={!tooltipText}>
|
||||
<div
|
||||
className={twMerge("flex w-80 flex-grow items-center py-1 pl-4 pr-2", className)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<span className="blur">********</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Blur } from "./Blur";
|
||||
@@ -120,6 +120,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
|
||||
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
|
||||
const { data: secrets } = useGetProjectSecrets({
|
||||
viewSecretValue: false,
|
||||
environment: suggestionSource.environment || "",
|
||||
secretPath: suggestionSource.secretPath || "",
|
||||
workspaceId,
|
||||
|
||||
@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretActions {
|
||||
DescribeSecret = "read",
|
||||
ReadValue = "readValue",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionDynamicSecretActions {
|
||||
ReadRootCredential = "read-root-credential",
|
||||
CreateRootCredential = "create-root-credential",
|
||||
@@ -138,7 +146,7 @@ export type SecretImportSubjectFields = {
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSecretActions,
|
||||
(
|
||||
| ProjectPermissionSub.Secrets
|
||||
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||
|
||||
@@ -207,6 +207,7 @@ export const useGetProjectSecretsDetails = (
|
||||
search = "",
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
viewSecretValue,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
tags
|
||||
@@ -231,6 +232,7 @@ export const useGetProjectSecretsDetails = (
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
viewSecretValue,
|
||||
offset,
|
||||
projectId,
|
||||
environment,
|
||||
@@ -247,6 +249,7 @@ export const useGetProjectSecretsDetails = (
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
viewSecretValue,
|
||||
offset,
|
||||
projectId,
|
||||
environment,
|
||||
|
||||
@@ -69,6 +69,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
"environments"
|
||||
> & {
|
||||
viewSecretValue: boolean;
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
tags: Record<string, boolean>;
|
||||
|
||||
@@ -79,6 +79,7 @@ export const decryptSecrets = (
|
||||
id: encSecret.id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
secretValueHidden: encSecret.secretValueHidden,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
|
||||
@@ -137,6 +137,7 @@ export const useGetImportedSecretsSingleEnv = ({
|
||||
env: encSecret.environment,
|
||||
key: encSecret.secretKey,
|
||||
value: encSecret.secretValue,
|
||||
secretValueHidden: encSecret.secretValueHidden,
|
||||
tags: encSecret.tags,
|
||||
comment: encSecret.secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
@@ -176,6 +177,7 @@ export const useGetImportedSecretsAllEnvs = ({
|
||||
env: encSecret.environment,
|
||||
key: encSecret.secretKey,
|
||||
value: encSecret.secretValue,
|
||||
secretValueHidden: encSecret.secretValueHidden,
|
||||
tags: encSecret.tags,
|
||||
comment: encSecret.secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
|
||||
@@ -75,6 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
|
||||
id: secretVersion.secretId,
|
||||
env: data.environment.slug,
|
||||
key: secretVersion.secretKey,
|
||||
secretValueHidden: secretVersion.secretValueHidden,
|
||||
value: secretVersion.secretValue || "",
|
||||
tags: secretVersion.tags,
|
||||
comment: secretVersion.secretComment,
|
||||
|
||||
@@ -26,8 +26,13 @@ import {
|
||||
|
||||
export const secretKeys = {
|
||||
// this is also used in secretSnapshot part
|
||||
getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
|
||||
[{ workspaceId, environment, secretPath }, "secrets"] as const,
|
||||
getProjectSecret: ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
viewSecretValue
|
||||
}: TGetProjectSecretsKey) =>
|
||||
[{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const,
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
|
||||
getSecretAccessList: ({
|
||||
workspaceId,
|
||||
@@ -44,13 +49,15 @@ export const fetchProjectSecrets = async ({
|
||||
environment,
|
||||
secretPath,
|
||||
includeImports,
|
||||
expandSecretReferences
|
||||
expandSecretReferences,
|
||||
viewSecretValue
|
||||
}: TGetProjectSecretsKey) => {
|
||||
const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", {
|
||||
params: {
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
viewSecretValue,
|
||||
expandSecretReferences,
|
||||
include_imports: includeImports
|
||||
}
|
||||
@@ -68,6 +75,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
|
||||
env: el.environment,
|
||||
key: el.secretKey,
|
||||
value: el.secretValue,
|
||||
secretValueHidden: el.secretValueHidden,
|
||||
tags: el.tags || [],
|
||||
comment: el.secretComment || "",
|
||||
reminderRepeatDays: el.secretReminderRepeatDays,
|
||||
@@ -107,6 +115,7 @@ export const useGetProjectSecrets = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
viewSecretValue,
|
||||
options
|
||||
}: TGetProjectSecretsDTO & {
|
||||
options?: Omit<
|
||||
@@ -123,8 +132,13 @@ export const useGetProjectSecrets = ({
|
||||
...options,
|
||||
// wait for all values to be available
|
||||
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
|
||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
||||
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||
queryKey: secretKeys.getProjectSecret({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
viewSecretValue
|
||||
}),
|
||||
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }),
|
||||
select: useCallback(
|
||||
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
|
||||
[]
|
||||
|
||||
@@ -19,6 +19,7 @@ export type EncryptedSecret = {
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHidden: boolean;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -37,6 +38,7 @@ export type SecretV3RawSanitized = {
|
||||
version: number;
|
||||
key: string;
|
||||
value?: string;
|
||||
secretValueHidden: boolean;
|
||||
comment?: string;
|
||||
reminderRepeatDays?: number | null;
|
||||
reminderNote?: string | null;
|
||||
@@ -61,6 +63,7 @@ export type SecretV3Raw = {
|
||||
environment: string;
|
||||
version: number;
|
||||
type: string;
|
||||
secretValueHidden: boolean;
|
||||
secretKey: string;
|
||||
secretPath: string;
|
||||
secretValue?: string;
|
||||
@@ -95,6 +98,7 @@ export type SecretVersions = {
|
||||
envId: string;
|
||||
secretKey: string;
|
||||
secretValue?: string;
|
||||
secretValueHidden: boolean;
|
||||
secretComment?: string;
|
||||
tags: WsTag[];
|
||||
__v: number;
|
||||
@@ -115,6 +119,7 @@ export type TGetProjectSecretsKey = {
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
includeImports?: boolean;
|
||||
viewSecretValue?: boolean;
|
||||
expandSecretReferences?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ export enum ApiErrorTypes {
|
||||
PermissionBoundaryError = "PermissionBoundaryError",
|
||||
BadRequestError = "BadRequest",
|
||||
UnauthorizedError = "UnauthorizedError",
|
||||
ForbiddenError = "PermissionDenied"
|
||||
ForbiddenError = "PermissionDenied",
|
||||
CustomForbiddenError = "ForbiddenError"
|
||||
}
|
||||
|
||||
export type TApiErrors =
|
||||
@@ -70,6 +71,12 @@ export type TApiErrors =
|
||||
details: PureAbility["rules"];
|
||||
statusCode: 403;
|
||||
}
|
||||
| {
|
||||
reqId: string;
|
||||
error: ApiErrorTypes.CustomForbiddenError;
|
||||
message: string;
|
||||
statusCode: 403;
|
||||
}
|
||||
| {
|
||||
reqId: string;
|
||||
statusCode: 400;
|
||||
|
||||
+7
-1
@@ -58,6 +58,7 @@ const schema = z.object({
|
||||
permissions: z
|
||||
.object({
|
||||
read: z.boolean(),
|
||||
readValue: z.boolean(),
|
||||
write: z.boolean()
|
||||
})
|
||||
.required()
|
||||
@@ -296,14 +297,19 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name="permissions"
|
||||
defaultValue={{
|
||||
read: true,
|
||||
readValue: false,
|
||||
write: false
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
const options = [
|
||||
{
|
||||
label: "Read (default)",
|
||||
label: "Describe Secret (default)",
|
||||
value: "read"
|
||||
},
|
||||
{
|
||||
label: "Read Value (optional)",
|
||||
value: "readValue"
|
||||
},
|
||||
{
|
||||
label: "Write (optional)",
|
||||
value: "write"
|
||||
|
||||
+15
-12
@@ -117,24 +117,27 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||
{actions.map(({ label, value }) => {
|
||||
if (typeof value !== "string") return undefined;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
key={`${el.id}-${label}`}
|
||||
name={`permissions.${subject}.${rootIndex}.${value}` as any}
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isDisabled={isDisabled}
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isDisabled={isDisabled}
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
+34
-2
@@ -9,6 +9,7 @@ import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
TPermissionCondition,
|
||||
TPermissionConditionOperators
|
||||
@@ -22,6 +23,14 @@ const GeneralPolicyActionSchema = z.object({
|
||||
create: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(), // describe secret
|
||||
edit: z.boolean().optional(),
|
||||
delete: z.boolean().optional(),
|
||||
create: z.boolean().optional(),
|
||||
readValue: z.boolean().optional()
|
||||
});
|
||||
|
||||
const CmekPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
edit: z.boolean().optional(),
|
||||
@@ -114,7 +123,7 @@ export const projectRoleFormSchema = z.object({
|
||||
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
|
||||
permissions: z
|
||||
.object({
|
||||
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
||||
[ProjectPermissionSub.Secrets]: SecretPolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
@@ -283,6 +292,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.Secrets) {
|
||||
const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret);
|
||||
const canEdit = action.includes(ProjectPermissionSecretActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionSecretActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionSecretActions.Create);
|
||||
const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
formVal[subject]!.push({
|
||||
read: canRead,
|
||||
create: canCreate,
|
||||
edit: canEdit,
|
||||
delete: canDelete,
|
||||
readValue: canReadValue,
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// for other subjects
|
||||
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||
@@ -483,8 +514,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
[ProjectPermissionSub.Secrets]: {
|
||||
title: "Secrets",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Describe Secret", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Read Value", value: "readValue" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
]
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@ import {
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
const typeSchema = z
|
||||
.object({
|
||||
@@ -275,7 +276,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
|
||||
isMulti
|
||||
options={environments.filter((environment) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: environment.slug,
|
||||
secretPath,
|
||||
|
||||
+26
-17
@@ -25,6 +25,7 @@ import {
|
||||
ModalTrigger,
|
||||
Tooltip
|
||||
} 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 { useToggle } from "@app/hooks";
|
||||
@@ -39,6 +40,7 @@ type Props = {
|
||||
isVisible?: boolean;
|
||||
isImportedSecret: boolean;
|
||||
environment: string;
|
||||
secretValueHidden: boolean;
|
||||
secretPath: string;
|
||||
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
||||
onSecretUpdate: (
|
||||
@@ -58,6 +60,7 @@ export const SecretEditRow = ({
|
||||
isImportedSecret,
|
||||
onSecretUpdate,
|
||||
secretName,
|
||||
secretValueHidden,
|
||||
onSecretCreate,
|
||||
onSecretDelete,
|
||||
environment,
|
||||
@@ -140,24 +143,29 @@ 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}
|
||||
value={field.value as string}
|
||||
key="secret-input"
|
||||
isVisible={isVisible}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
isImport={isImportedSecret}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{secretValueHidden ? (
|
||||
<Blur tooltipText="You do not have permission to read the value of this secret." />
|
||||
) : (
|
||||
<Controller
|
||||
disabled={isImportedSecret && !defaultValue}
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
isReadOnly={isImportedSecret}
|
||||
value={field.value as string}
|
||||
key="secret-input"
|
||||
isVisible={isVisible}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
isImport={isImportedSecret}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-24 justify-center space-x-3 pl-2 transition-all",
|
||||
@@ -211,6 +219,7 @@ export const SecretEditRow = ({
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Copy Secret">
|
||||
<IconButton
|
||||
isDisabled={secretValueHidden}
|
||||
ariaLabel="copy-value"
|
||||
onClick={handleCopySecretToClipboard}
|
||||
variant="plain"
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
|
||||
type Props = {
|
||||
environments: { name: string; slug: string }[];
|
||||
@@ -25,7 +26,7 @@ export const SecretNoAccessOverviewTableRow = ({ environments = [], count }: Pro
|
||||
<div className="text-bunker-300">
|
||||
<FontAwesomeIcon className="block" icon={faLock} />
|
||||
</div>
|
||||
<div className="blur-sm">NO ACCESS</div>
|
||||
<Blur />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
+6
-3
@@ -221,10 +221,13 @@ export const SecretOverviewTableRow = ({
|
||||
secretPath={secretPath}
|
||||
isVisible={isSecretVisible}
|
||||
secretName={secretKey}
|
||||
secretValueHidden={secret?.secretValueHidden || false}
|
||||
defaultValue={
|
||||
secret?.valueOverride ||
|
||||
secret?.value ||
|
||||
importedSecret?.secret?.value
|
||||
secret?.secretValueHidden
|
||||
? ""
|
||||
: secret?.valueOverride ||
|
||||
secret?.value ||
|
||||
importedSecret?.secret?.value
|
||||
}
|
||||
secretId={secret?.id}
|
||||
isOverride={Boolean(secret?.valueOverride)}
|
||||
|
||||
+4
-8
@@ -10,12 +10,8 @@ import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
@@ -55,8 +51,8 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
|
||||
secretTags: (secretDetails?.tags || []).map((i) => i.slug)
|
||||
});
|
||||
const isSecretInEnvReadOnly =
|
||||
permission.can(ProjectPermissionActions.Read, secretPermissionSubject) &&
|
||||
permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject);
|
||||
permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) &&
|
||||
permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject);
|
||||
if (isSecretInEnvReadOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+41
-21
@@ -110,21 +110,31 @@ export const QuickSearchSecretItem = ({
|
||||
</Badge>
|
||||
)}
|
||||
{isSingleEnv ? (
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="Copy secret value"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const el = envSlugMap.get(groupSecret.env)?.name;
|
||||
if (el) {
|
||||
handleCopy(groupSecret.value!, el);
|
||||
}
|
||||
}}
|
||||
<Tooltip
|
||||
isDisabled={!groupSecret?.secretValueHidden}
|
||||
content={
|
||||
groupSecret?.secretValueHidden
|
||||
? "You do not have permission to view this secret value"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="md"
|
||||
isDisabled={groupSecret?.secretValueHidden}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="Copy secret value"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const el = envSlugMap.get(groupSecret.env)?.name;
|
||||
if (el) {
|
||||
handleCopy(groupSecret.value!, el);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -158,14 +168,24 @@ export const QuickSearchSecretItem = ({
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="View secret value"
|
||||
<Tooltip
|
||||
isDisabled={!groupSecret?.secretValueHidden}
|
||||
content={
|
||||
groupSecret?.secretValueHidden
|
||||
? "You do not have permission to view this secret value"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="md"
|
||||
isDisabled={groupSecret?.secretValueHidden}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="View secret value"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Hover to Reveal...</DropdownMenuLabel>
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
||||
import {
|
||||
@@ -58,7 +59,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
// user should have the ability to delete secrets/folders in at least one of the envs
|
||||
const shouldShowDelete = userAvailableEnvs.some((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env.slug,
|
||||
secretPath,
|
||||
@@ -110,7 +111,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
|
||||
const entry = secretRecord[env.slug];
|
||||
const canDeleteSecret = permission.can(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env.slug,
|
||||
secretPath,
|
||||
|
||||
+3
-2
@@ -25,7 +25,8 @@ import {
|
||||
Spinner,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useMoveSecrets } from "@app/hooks/api";
|
||||
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
|
||||
@@ -95,7 +96,7 @@ const Content = ({
|
||||
env.slug,
|
||||
{
|
||||
missingPermissions: permission.cannot(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env.slug,
|
||||
secretPath: sourceSecretPath,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useGetImportedSecretsSingleEnv,
|
||||
@@ -103,7 +104,16 @@ const Page = () => {
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const secretPath = (routerQueryParams.secretPath as string) || "/";
|
||||
const canReadSecret = permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})
|
||||
);
|
||||
const canReadSecretValue = permission.can(
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -176,6 +186,7 @@ const Page = () => {
|
||||
orderDirection,
|
||||
includeImports: canReadSecretImports && filter.include.import,
|
||||
includeFolders: filter.include.folder,
|
||||
viewSecretValue: canReadSecretValue,
|
||||
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
|
||||
includeSecrets: canReadSecret && filter.include.secret,
|
||||
tags: filter.tags
|
||||
|
||||
+62
-41
@@ -21,6 +21,7 @@ import {
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AxiosError } from "axios";
|
||||
import FileSaver from "file-saver";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -54,7 +55,7 @@ import {
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
|
||||
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretType, WsTag } from "@app/hooks/api/types";
|
||||
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
|
||||
import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput";
|
||||
|
||||
import {
|
||||
@@ -152,51 +153,71 @@ export const ActionBar = ({
|
||||
};
|
||||
|
||||
const handleSecretDownload = async () => {
|
||||
const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
|
||||
workspaceId,
|
||||
expandSecretReferences: true,
|
||||
includeImports: true,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
const secretsPicked = new Set<string>();
|
||||
const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
|
||||
localSecrets.forEach((el) => {
|
||||
secretsPicked.add(el.secretKey);
|
||||
secretsToDownload.push({
|
||||
key: el.secretKey,
|
||||
value: el.secretValue,
|
||||
comment: el.secretComment
|
||||
try {
|
||||
const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
|
||||
workspaceId,
|
||||
expandSecretReferences: true,
|
||||
includeImports: true,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
const secretsPicked = new Set<string>();
|
||||
const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
|
||||
localSecrets.forEach((el) => {
|
||||
secretsPicked.add(el.secretKey);
|
||||
secretsToDownload.push({
|
||||
key: el.secretKey,
|
||||
value: el.secretValue,
|
||||
comment: el.secretComment
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
|
||||
const secret = localImportedSecrets[i].secrets[j];
|
||||
if (!secretsPicked.has(secret.secretKey)) {
|
||||
secretsToDownload.push({
|
||||
key: secret.secretKey,
|
||||
value: secret.secretValue,
|
||||
comment: secret.secretComment
|
||||
});
|
||||
for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
|
||||
const secret = localImportedSecrets[i].secrets[j];
|
||||
if (!secretsPicked.has(secret.secretKey)) {
|
||||
secretsToDownload.push({
|
||||
key: secret.secretKey,
|
||||
value: secret.secretValue,
|
||||
comment: secret.secretComment
|
||||
});
|
||||
}
|
||||
secretsPicked.add(secret.secretKey);
|
||||
}
|
||||
secretsPicked.add(secret.secretKey);
|
||||
}
|
||||
|
||||
const file = secretsToDownload
|
||||
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
|
||||
.reduce(
|
||||
(prev, { key, comment, value }, index) =>
|
||||
prev +
|
||||
(comment
|
||||
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
|
||||
: `${key}=${value}\n`),
|
||||
""
|
||||
);
|
||||
|
||||
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
|
||||
FileSaver.saveAs(blob, `${environment}.env`);
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError) {
|
||||
const error = err?.response?.data as TApiErrors;
|
||||
|
||||
if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) {
|
||||
createNotification({
|
||||
title: "You don't have permission to download secrets",
|
||||
text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
createNotification({
|
||||
title: "Failed to download secrets",
|
||||
text: "Please try again later.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
const file = secretsToDownload
|
||||
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
|
||||
.reduce(
|
||||
(prev, { key, comment, value }, index) =>
|
||||
prev +
|
||||
(comment
|
||||
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
|
||||
: `${key}=${value}\n`),
|
||||
""
|
||||
);
|
||||
|
||||
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
|
||||
FileSaver.saveAs(blob, `${environment}.env`);
|
||||
};
|
||||
|
||||
const handleSecretBulkDelete = async () => {
|
||||
|
||||
+117
-57
@@ -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">
|
||||
|
||||
+20
-4
@@ -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"
|
||||
|
||||
+2
-7
@@ -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>
|
||||
))}
|
||||
|
||||
+17
-2
@@ -20,6 +20,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
|
||||
@@ -120,11 +121,25 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
|
||||
<Td className="border-r border-mineshaft-600">Value</Td>
|
||||
{isModified && (
|
||||
<Td className="border-r border-mineshaft-600">
|
||||
<SecretInput value={preSecret?.value} />
|
||||
{preSecret?.secretValueHidden ? (
|
||||
<Blur
|
||||
className="w-min"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<SecretInput value={preSecret?.value} />
|
||||
)}
|
||||
</Td>
|
||||
)}
|
||||
<Td>
|
||||
<SecretInput value={postSecret?.value} />
|
||||
{postSecret?.secretValueHidden ? (
|
||||
<Blur
|
||||
className="w-min"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<SecretInput value={postSecret?.value} />
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
{Boolean(preSecret?.idOverride || postSecret?.idOverride) && (
|
||||
|
||||
Reference in New Issue
Block a user