fix: view secret value permission (requested changes)

This commit is contained in:
Daniel Hougaard
2025-03-05 00:17:51 +04:00
parent 96046726b2
commit 4b3efb43b0
18 changed files with 412 additions and 216 deletions

View File

@ -221,20 +221,25 @@ export async function down(knex: Knex): Promise<void> {
}, []);
if (updatedServiceTokens.length > 0) {
await knex(TableName.ServiceToken)
.whereIn(
"id",
updatedServiceTokens.map((t) => t.id)
)
.update({
// @ts-expect-error -- raw query
permissions: knex.raw(
`CASE id
${updatedServiceTokens.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
END`,
updatedServiceTokens.map((t) => t.permissions)
for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) {
const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.ServiceToken)
.whereIn(
"id",
chunk.map((t) => t.id)
)
});
.update({
// @ts-expect-error -- raw query
permissions: knex.raw(
`CASE id
${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
END`,
chunk.map((t) => t.permissions)
)
});
}
}
const updatedRoles = projectRoles.reduce<typeof projectRoles>((acc, projectRole) => {

View File

@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
secretVersions: secretRawSchema.array()
secretVersions: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
})
}
},
@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
offset: req.query.offset,
secretId: req.params.secretId
});
return { secretVersions };
}
});

View File

@ -1,10 +1,10 @@
import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
import { SecretSnapshotsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@ -33,11 +33,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
.extend({
secretValueHidden: z.boolean(),
secretId: z.string(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
}).array()
tags: SanitizedTagSchema.array()
})
.array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),

View File

@ -442,7 +442,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe(
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV1Schema.describe(

View File

@ -417,6 +417,8 @@ export const secretV2BridgeServiceFactory = ({
if ((inputSecret.tagIds || []).length !== newTags.length)
throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` });
const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags;
// now check with new ids
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit,
@ -424,7 +426,9 @@ export const secretV2BridgeServiceFactory = ({
environment,
secretPath,
secretName: inputSecret.secretName,
secretTags: newTags?.map((el) => el.slug)
...(tagsToCheck.length && {
secretTags: tagsToCheck.map((el) => el.slug)
})
})
);
@ -441,7 +445,9 @@ export const secretV2BridgeServiceFactory = ({
environment,
secretPath,
secretName: inputSecret.newSecretName,
secretTags: newTags?.map((el) => el.slug)
...(tagsToCheck.length && {
secretTags: tagsToCheck.map((el) => el.slug)
})
})
);
}
@ -519,14 +525,15 @@ export const secretV2BridgeServiceFactory = ({
});
}
const tagsToCheck = newTags?.length ? newTags : secret.tags;
const secretValueHidden = !permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: inputSecret.secretName,
secretTags: tagsToCheck.length ? tagsToCheck.map((el) => el.slug) : undefined
...(tagsToCheck.length && {
secretTags: tagsToCheck.map((el) => el.slug)
})
})
);
@ -1073,26 +1080,27 @@ export const secretV2BridgeServiceFactory = ({
expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
return (
permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
) &&
permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
const canDescribe = permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
);
const canReadValue = permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
);
return viewSecretValue ? canDescribe && canReadValue : canDescribe;
}
});
@ -1220,26 +1228,26 @@ export const secretV2BridgeServiceFactory = ({
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
return (
permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
) &&
permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
const canDescribe = permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
);
const canReadValue = permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
);
return viewSecretValue ? canDescribe && canReadValue : canDescribe;
}
});
@ -1461,7 +1469,7 @@ export const secretV2BridgeServiceFactory = ({
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
secretTags: el.tags?.map((i) => i.slug)
})
);
@ -1970,9 +1978,24 @@ export const secretV2BridgeServiceFactory = ({
projectId: folder.projectId
});
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
return secretVersions.map((el) =>
reshapeBridgeSecret(
const secretVersions = await secretVersionDAL.findBySecretId(secretId, {
offset,
limit,
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
const secretValueHidden = permission.cannot(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: folder.environment.envSlug,
secretPath: "/",
secretName: el.key,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
})
})
);
return reshapeBridgeSecret(
folder.projectId,
folder.environment.envSlug,
"/",
@ -1981,9 +2004,9 @@ export const secretV2BridgeServiceFactory = ({
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
},
false
)
);
secretValueHidden
);
});
};
// this is a backfilling API for secret references

View File

@ -1,9 +1,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
@ -12,6 +12,58 @@ export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2Bridge
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersionsV2> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersionV2)
.where(`${TableName.SecretVersionV2}.secretId`, secretId)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try {
@ -124,6 +176,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
pruneExcessVersions,
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId
findLatestVersionByFolderId,
findBySecretId
};
};

View File

@ -667,15 +667,7 @@ export const secretServiceFactory = ({
environment,
secretPath: groupedPaths[secret.folderId][0].path
})),
imports: importedSecrets.map((el) => {
return {
...el,
secrets: el.secrets.map((secret) => ({
...secret,
secretValueHidden: false
}))
};
})
imports: importedSecrets
};
}
@ -2399,19 +2391,42 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
return secretVersions.map((el) =>
decryptSecretRaw(
const secretVersions = await secretVersionDAL.findBySecretId(secretId, {
offset,
limit,
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
});
const secretValueHidden = permission.cannot(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: folder.environment.envSlug,
secretPath: "/",
secretName: secretKey,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
})
})
);
return decryptSecretRaw(
{
secretValueHidden: false,
secretValueHidden,
...el,
workspace: folder.projectId,
environment: folder.environment.envSlug,
secretPath: "/"
},
botKey
)
);
);
});
};
const attachTags = async ({

View File

@ -1,9 +1,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory
export const secretVersionDALFactory = (db: TDbClient) => {
const secretVersionOrm = ormify(db, TableName.SecretVersion);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersions> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersion)
.where(`${TableName.SecretVersion}.secretId`, secretId)
.leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`)
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.select(selectAllTableCols(TableName.SecretVersion))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try {
@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId,
findBySecretId,
bulkUpdateNoVersionIncrement
};
};

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { Blur } from "./Blur";

View File

@ -75,7 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
id: secretVersion.secretId,
env: data.environment.slug,
key: secretVersion.secretKey,
secretValueHidden: false,
secretValueHidden: secretVersion.secretValueHidden,
value: secretVersion.secretValue || "",
tags: secretVersion.tags,
comment: secretVersion.secretComment,

View File

@ -98,6 +98,7 @@ export type SecretVersions = {
envId: string;
secretKey: string;
secretValue?: string;
secretValueHidden: boolean;
secretComment?: string;
tags: WsTag[];
__v: number;

View File

@ -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";
@ -143,11 +144,7 @@ export const SecretEditRow = ({
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
{secretValueHidden ? (
<Tooltip content="You do not have permission to read the value of this secret.">
<div className="flex w-80 flex-grow items-center py-1 pl-4 pr-2">
<span className="blur">********</span>
</div>
</Tooltip>
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : (
<Controller
disabled={isImportedSecret && !defaultValue}

View File

@ -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>

View File

@ -16,6 +16,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } 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";
@ -207,9 +208,16 @@ export const SecretDetailSidebar = ({
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
};
const handleReminderSubmit = async (reminderRepeatDays: number | null | undefined, reminderNote: string | null | undefined) => {
await onSaveSecret(secret, { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, () => { });
}
const handleReminderSubmit = async (
reminderRepeatDays: number | null | undefined,
reminderNote: string | null | undefined
) => {
await onSaveSecret(
secret,
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true },
() => {}
);
};
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
@ -228,7 +236,7 @@ export const SecretDetailSidebar = ({
if (data) {
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
setValue("reminderNote", data.note, { shouldDirty: false });
handleReminderSubmit(data.days, data.note)
handleReminderSubmit(data.days, data.note);
}
}}
/>
@ -278,6 +286,7 @@ export const SecretDetailSidebar = ({
render={({ field }) => (
<div className="flex items-center gap-2">
<FormControl
className="flex-1"
helperText={
cannotReadSecretValue ? (
<div className="flex space-x-2">
@ -651,51 +660,34 @@ 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 }) => (
<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 }) => (
<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}
</div>
</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 flex-row">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
Value:
<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="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"
onClick={(e) => {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
<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"
onClick={(e) => {
if (secretValueHidden) return;
// 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 (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
@ -713,72 +705,109 @@ export const SecretDetailSidebar = ({
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
{secretValue}
</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 === " ") {
}}
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);
}
}}
>
<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"
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">
{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>
<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">

View File

@ -46,6 +46,7 @@ import {
} from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import {
FontAwesomeSpriteName,
formSchema,
@ -283,15 +284,7 @@ export const SecretItem = memo(
)}
/>
) : secretValueHidden ? (
<Tooltip content="You do not have permission to read the value of this secret.">
<div
className="flex w-80 flex-grow items-center py-1 pl-4 pr-2"
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
</Tooltip>
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : (
<Controller
name="value"

View File

@ -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>
))}

View File

@ -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) && (