requested changes

This commit is contained in:
Daniel Hougaard
2025-03-11 04:52:12 +04:00
parent b9b76579ac
commit 483fb458dd
12 changed files with 441 additions and 191 deletions
@@ -23,12 +23,6 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
subjectFields?: SecretSubjectFields subjectFields?: SecretSubjectFields
) { ) {
try { try {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
} else {
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
}
} catch {
if (subjectFields) { if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeAndReadValue,
@@ -40,6 +34,12 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
ProjectPermissionSub.Secrets ProjectPermissionSub.Secrets
); );
} }
} catch {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
} else {
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
}
} }
} }
@@ -834,6 +834,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/accessible-secrets",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
filterByAction: z
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
.default(ProjectPermissionSecretActions.ReadValue)
}),
response: {
200: z.object({
secrets: secretRawSchema
.extend({
secretPath: z.string().optional()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId, environment, secretPath, filterByAction } = req.query;
const { secrets } = await server.services.secret.getAccessibleSecrets({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment,
secretPath,
projectId,
filterByAction
});
return { secrets };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/secrets-by-keys", url: "/secrets-by-keys",
@@ -60,6 +60,7 @@ import {
TCreateSecretDTO, TCreateSecretDTO,
TDeleteManySecretDTO, TDeleteManySecretDTO,
TDeleteSecretDTO, TDeleteSecretDTO,
TGetAccessibleSecretsDTO,
TGetASecretDTO, TGetASecretDTO,
TGetSecretReferencesTreeDTO, TGetSecretReferencesTreeDTO,
TGetSecretsDTO, TGetSecretsDTO,
@@ -200,7 +201,7 @@ export const secretV2BridgeServiceFactory = ({
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
references.forEach((el) => { references.forEach((el) => {
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: el.environment, environment: el.environment,
secretPath: el.secretPath, secretPath: el.secretPath,
secretName: el.secretKey, secretName: el.secretKey,
@@ -1291,7 +1292,7 @@ export const secretV2BridgeServiceFactory = ({
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: expandEnvironment, environment: expandEnvironment,
@@ -1347,7 +1348,7 @@ export const secretV2BridgeServiceFactory = ({
let secretValue = secret.encryptedValue let secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: ""; : "";
if (shouldExpandSecretReferences && secretValue) { if (shouldExpandSecretReferences && secretValue && viewSecretValue) {
// eslint-disable-next-line // eslint-disable-next-line
const expandedSecretValue = await expandSecretReferences({ const expandedSecretValue = await expandSecretReferences({
environment, environment,
@@ -2613,6 +2614,86 @@ export const secretV2BridgeServiceFactory = ({
return { tree: stackTrace, value: expandedValue }; return { tree: stackTrace, value: expandedValue };
}; };
const getAccessibleSecrets = async ({
projectId,
secretPath,
environment,
filterByAction,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TGetAccessibleSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath
});
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return { secrets: [] };
const secrets = await secretDAL.findByFolderIds([folder.id]);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedSecrets = secrets
.filter((el) => {
if (
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
) {
return false;
}
if (filterByAction === ProjectPermissionSecretActions.ReadValue) {
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
});
}
return true;
})
.map((secret) => {
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
},
false
);
});
return {
secrets: decryptedSecrets
};
};
return { return {
createSecret, createSecret,
deleteSecret, deleteSecret,
@@ -2630,6 +2711,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsMultiEnv, getSecretsMultiEnv,
getSecretReferenceTree, getSecretReferenceTree,
getSecretsByFolderMappings, getSecretsByFolderMappings,
getSecretById getSecretById,
getAccessibleSecrets
}; };
}; };
@@ -351,3 +351,10 @@ export type TGetSecretsRawByFolderMappingsDTO = {
filters: TFindSecretsByFolderIdsFilter; filters: TFindSecretsByFolderIdsFilter;
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
}; };
export type TGetAccessibleSecretsDTO = {
environment: string;
projectId: string;
secretPath: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
+36 -1
View File
@@ -81,6 +81,7 @@ import {
TDeleteManySecretRawDTO, TDeleteManySecretRawDTO,
TDeleteSecretDTO, TDeleteSecretDTO,
TDeleteSecretRawDTO, TDeleteSecretRawDTO,
TGetAccessibleSecretsDTO,
TGetASecretByIdRawDTO, TGetASecretByIdRawDTO,
TGetASecretDTO, TGetASecretDTO,
TGetASecretRawDTO, TGetASecretRawDTO,
@@ -1312,6 +1313,39 @@ export const secretServiceFactory = ({
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess }; return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
}; };
const getAccessibleSecrets = async ({
projectId,
secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge) {
throw new BadRequestError({
message: "Project version does not support this endpoint.",
name: "ProjectVersionNotSupported"
});
}
const secrets = await secretV2BridgeService.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction,
actor,
actorId,
actorOrgId,
actorAuthMethod
});
return secrets;
};
const getSecretsRaw = async ({ const getSecretsRaw = async ({
projectId, projectId,
path, path,
@@ -3261,6 +3295,7 @@ export const secretServiceFactory = ({
getSecretReferenceTree, getSecretReferenceTree,
getSecretsRawByFolderMappings, getSecretsRawByFolderMappings,
getSecretAccessList, getSecretAccessList,
getSecretByIdRaw getSecretByIdRaw,
getAccessibleSecrets
}; };
}; };
@@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">; type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
@@ -180,6 +181,12 @@ export enum SecretsOrderBy {
Name = "name" // "key" for secrets but using name for use across resources Name = "name" // "key" for secrets but using name for use across resources
} }
export type TGetAccessibleSecretsDTO = {
secretPath: string;
environment: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
export type TGetSecretsRawDTO = { export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
path: string; path: string;
@@ -1,4 +1,5 @@
export { export {
useGetAccessibleSecrets,
useGetProjectSecretsDetails, useGetProjectSecretsDetails,
useGetProjectSecretsOverview, useGetProjectSecretsOverview,
useGetProjectSecretsQuickSearch useGetProjectSecretsQuickSearch
+56 -1
View File
@@ -11,6 +11,7 @@ import {
DashboardSecretsOrderBy, DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch, TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse, TDashboardProjectSecretsQuickSearchResponse,
TGetAccessibleSecretsDTO,
TGetDashboardProjectSecretsByKeys, TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO, TGetDashboardProjectSecretsOverviewDTO,
@@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { groupBy, unique } from "@app/lib/fn/array"; import { groupBy, unique } from "@app/lib/fn/array";
import { SecretV3Raw } from "../types";
export const dashboardKeys = { export const dashboardKeys = {
all: () => ["dashboard"] as const, all: () => ["dashboard"] as const,
getDashboardSecrets: ({ getDashboardSecrets: ({
@@ -58,7 +61,14 @@ export const dashboardKeys = {
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }), ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
"quick-search", "quick-search",
params params
] as const ] as const,
getAccessibleSecrets: ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) =>
[...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const
}; };
export const fetchProjectSecretsOverview = async ({ export const fetchProjectSecretsOverview = async ({
@@ -295,6 +305,22 @@ export const fetchProjectSecretsQuickSearch = async ({
return data; return data;
}; };
const fetchAccessibleSecrets = async ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>(
"/api/v1/dashboard/accessible-secrets",
{
params: { projectId, secretPath, environment, filterByAction }
}
);
return data.secrets;
};
export const useGetProjectSecretsQuickSearch = ( export const useGetProjectSecretsQuickSearch = (
{ {
projectId, projectId,
@@ -357,3 +383,32 @@ export const useGetProjectSecretsQuickSearch = (
placeholderData: (previousData) => previousData placeholderData: (previousData) => previousData
}); });
}; };
export const useGetAccessibleSecrets = ({
projectId,
secretPath,
environment,
filterByAction,
options
}: TGetAccessibleSecretsDTO & {
options?: Omit<
UseQueryOptions<
SecretV3Raw[],
unknown,
SecretV3Raw[],
ReturnType<typeof dashboardKeys.getAccessibleSecrets>
>,
"queryKey" | "queryFn"
>;
}) => {
return useQuery({
...options,
queryKey: dashboardKeys.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction
}),
queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction })
});
};
+10
View File
@@ -1,3 +1,4 @@
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -101,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = {
environment: string; environment: string;
keys: string[]; keys: string[];
}; };
export type TGetAccessibleSecretsDTO = {
projectId: string;
secretPath: string;
environment: string;
filterByAction:
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;
};
@@ -27,9 +27,11 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur"; import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { SecretType } from "@app/hooks/api/types"; import { SecretType } from "@app/hooks/api/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
type Props = { type Props = {
defaultValue?: string | null; defaultValue?: string | null;
@@ -79,6 +81,9 @@ export const SecretEditRow = ({
value: defaultValue || null value: defaultValue || null
} }
}); });
const { permission } = useProjectPermission();
const [isDeleting, setIsDeleting] = useToggle(); const [isDeleting, setIsDeleting] = useToggle();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
@@ -120,6 +125,11 @@ export const SecretEditRow = ({
reset({ value }); reset({ value });
}; };
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue
);
const handleDeleteSecret = useCallback(async () => { const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on(); setIsDeleting.on();
setIsModalOpen(false); setIsModalOpen(false);
@@ -229,48 +239,43 @@ export const SecretEditRow = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
<ProjectPermissionCan
I={ProjectPermissionActions.Read} <div className="opacity-0 group-hover:opacity-100">
a={ProjectPermissionSub.Secrets} <Modal>
> <ModalTrigger asChild>
{(isAllowed) => ( <div className="opacity-0 group-hover:opacity-100">
<div className="opacity-0 group-hover:opacity-100"> <Tooltip
<Modal> content={
<ModalTrigger asChild> hasSecretReference(defaultValue || "")
<div className="opacity-0 group-hover:opacity-100"> ? "Secret Reference Tree"
<Tooltip : "Secret does not contain references"
content={ }
hasSecretReference(defaultValue || "")
? "Secret Reference Tree"
: "Secret does not contain references"
}
>
<IconButton
variant="plain"
ariaLabel="reference-tree"
className="h-full"
isDisabled={!hasSecretReference(defaultValue || "") || !isAllowed}
>
<FontAwesomeIcon icon={faProjectDiagram} />
</IconButton>
</Tooltip>
</div>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
> >
<SecretReferenceTree <IconButton
secretPath={secretPath} variant="plain"
environment={environment} ariaLabel="reference-tree"
secretKey={secretName} className="h-full"
/> isDisabled={!hasSecretReference(defaultValue || "") || !canReadSecretValue}
</ModalContent> >
</Modal> <FontAwesomeIcon icon={faProjectDiagram} />
</div> </IconButton>
)} </Tooltip>
</ProjectPermissionCan> </div>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={secretName}
/>
</ModalContent>
</Modal>
</div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { a={subject(ProjectPermissionSub.Secrets, {
@@ -20,8 +20,9 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce } from "@app/hooks"; import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api"; import { useGetAccessibleSecrets } from "@app/hooks/api/dashboard";
const formSchema = z.object({ const formSchema = z.object({
environment: z.object({ name: z.string(), slug: z.string() }), environment: z.object({ name: z.string(), slug: z.string() }),
@@ -32,7 +33,7 @@ const formSchema = z.object({
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
), ),
secrets: z secrets: z
.object({ key: z.string(), value: z.string().optional() }) .object({ secretKey: z.string(), secretValue: z.string().optional() })
.array() .array()
.min(1, "Select one or more secrets to copy") .min(1, "Select one or more secrets to copy")
}); });
@@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({
const selectedEnvSlug = watch("environment"); const selectedEnvSlug = watch("environment");
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath); const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({ const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } =
workspaceId, useGetAccessibleSecrets({
environment: selectedEnvSlug.slug, projectId: workspaceId,
secretPath: debouncedEnvCopySecretPath, secretPath: debouncedEnvCopySecretPath,
options: { environment: selectedEnvSlug.slug,
enabled: filterByAction: shouldIncludeValues
Boolean(workspaceId) && ? ProjectPermissionSecretActions.ReadValue
Boolean(selectedEnvSlug) && : ProjectPermissionSecretActions.DescribeSecret,
Boolean(debouncedEnvCopySecretPath) && options: {
isOpen enabled:
} Boolean(workspaceId) &&
}); Boolean(selectedEnvSlug) &&
Boolean(debouncedEnvCopySecretPath) &&
isOpen
}
});
useEffect(() => { useEffect(() => {
setValue("secrets", []); setValue("secrets", []);
}, [debouncedEnvCopySecretPath, selectedEnvSlug]); }, [debouncedEnvCopySecretPath, selectedEnvSlug]);
const handleSecSelectAll = () => { const handleSecSelectAll = () => {
if (secrets) { if (accessibleSecrets) {
setValue("secrets", secrets, { shouldDirty: true }); setValue("secrets", accessibleSecrets, { shouldDirty: true });
} }
}; };
const handleFormSubmit = async (data: TFormSchema) => { const handleFormSubmit = async (data: TFormSchema) => {
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {}; const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
data.secrets.forEach(({ key, value }) => { data.secrets.forEach(({ secretKey, secretValue }) => {
secretsToBePulled[key] = { secretsToBePulled[secretKey] = {
value: (shouldIncludeValues && value) || "", value: (shouldIncludeValues && secretValue) || "",
comments: [""] comments: [""]
}; };
}); });
@@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({
<FilterableSelect <FilterableSelect
placeholder={ placeholder={
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
isSecretsLoading isAccessibleSecretsLoading
? "Loading secrets..." ? "Loading secrets..."
: secrets?.length : accessibleSecrets?.length
? "Select secrets..." ? "Select secrets..."
: "No secrets found..." : "No secrets found..."
} }
isLoading={isSecretsLoading} isLoading={isAccessibleSecretsLoading}
options={secrets} options={accessibleSecrets}
value={value} value={value}
onChange={onChange} onChange={onChange}
isMulti isMulti
getOptionValue={(option) => option.key} getOptionValue={(option) => option.secretKey}
getOptionLabel={(option) => option.key} getOptionLabel={(option) => option.secretKey}
/> />
</FormControl> </FormControl>
)} )}
@@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({
<Switch <Switch
id="populate-include-value" id="populate-include-value"
isChecked={shouldIncludeValues} isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)} onCheckedChange={(isChecked) => {
setValue("secrets", []);
setShouldIncludeValues(isChecked as boolean);
}}
> >
Include secret values Include secret values
</Switch> </Switch>
@@ -13,8 +13,8 @@ import {
faShare, faShare,
faTag, faTag,
faTrash, faTrash,
faUser, faTriangleExclamation,
faTriangleExclamation faUser
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -728,8 +728,8 @@ export const SecretDetailSidebar = ({
<div className="mb-2 pl-1">Version History</div> <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]"> <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( {secretVersion?.map(
({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => ( ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
<div key={`secret-version-${index + 1}`} className="flex flex-row"> <div className="flex flex-row">
<div key={id} className="flex w-full flex-col space-y-1"> <div key={id} className="flex w-full flex-col space-y-1">
<div className="flex items-center"> <div className="flex items-center">
<div className="w-10"> <div className="w-10">
@@ -739,72 +739,50 @@ export const SecretDetailSidebar = ({
</div> </div>
<div>{format(new Date(createdAt), "Pp")}</div> <div>{format(new Date(createdAt), "Pp")}</div>
</div> </div>
<div>{format(new Date(createdAt), "Pp")}</div> <div className="flex w-full cursor-default">
</div> <div className="relative w-10">
<div className="flex w-full cursor-default"> <div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
<div className="relative w-10"> </div>
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" /> <div className="flex w-full cursor-default flex-col">
</div> {actor && (
<div className="flex w-full cursor-default flex-col"> <div className="flex flex-row">
{actor && ( <div className="flex w-fit flex-row text-sm">
<div className="flex flex-row"> Modified by:
<div className="flex w-fit flex-row text-sm"> <Tooltip
Modified by: content={getModifiedByName(actor.actorType, actor.name)}
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
onClick={() =>
onModifyHistoryClick(
actor.actorId,
actor.actorType,
actor.membershipId
)
}
className="cursor-pointer"
> >
<FontAwesomeIcon {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
icon={getModifiedByIcon(actor.actorType)} <div
className="ml-2" onClick={() =>
/> onModifyHistoryClick(
</div> actor.actorId,
</Tooltip> actor.actorType,
actor.membershipId
)
}
className="cursor-pointer"
>
<FontAwesomeIcon
icon={getModifiedByIcon(actor.actorType)}
className="ml-2"
/>
</div>
</Tooltip>
</div>
</div> </div>
</div> )}
)} <div className="flex flex-row">
<div className="flex flex-row"> <div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70"> Value:
Value: </div>
</div> <div className="group break-all pl-1 font-mono">
<div className="group break-all pl-1 font-mono"> <div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline"> <button
<button type="button"
type="button" className="select-none text-left"
className="select-none text-left" onClick={(e) => {
onClick={(e) => { if (secretValueHidden) return;
if (secretValueHidden) return;
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}}
onKeyDown={(e) => {
if (secretValueHidden) return;
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || ""); navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget; const target = e.currentTarget;
target.style.borderBottom = "1px dashed"; target.style.borderBottom = "1px dashed";
@@ -822,13 +800,30 @@ export const SecretDetailSidebar = ({
popup.remove(); popup.remove();
target.style.borderBottom = "none"; target.style.borderBottom = "none";
}, 3000); }, 3000);
} }}
}} onKeyDown={(e) => {
> if (secretValueHidden) return;
<Tooltip
className="break-normal text-xs" if (e.key === "Enter" || e.key === " ") {
content="You do not have permission to view this secret value" navigator.clipboard.writeText(secretValue || "");
isDisabled={!secretValueHidden} const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
> >
<span <span
className={twMerge( className={twMerge(
@@ -837,50 +832,50 @@ export const SecretDetailSidebar = ({
> >
{secretValueHidden ? "Hidden" : secretValue} {secretValueHidden ? "Hidden" : secretValue}
</span> </span>
</Tooltip> </button>
</button> <button
<button type="button"
type="button" className="ml-1 cursor-pointer"
className="ml-1 cursor-pointer" onClick={(e) => {
onClick={(e) => {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation(); e.stopPropagation();
e.currentTarget e.currentTarget
.closest(".group") .closest(".group")
?.classList.remove("show-value"); ?.classList.remove("show-value");
} }}
}} onKeyDown={(e) => {
> if (e.key === "Enter" || e.key === " ") {
<FontAwesomeIcon icon={faEyeSlash} /> e.stopPropagation();
</button> e.currentTarget
</div> .closest(".group")
<span className="group-[.show-value]:hidden"> ?.classList.remove("show-value");
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} }
<button }}
type="button" >
className="ml-1 cursor-pointer" <FontAwesomeIcon icon={faEyeSlash} />
onClick={(e) => { </button>
e.currentTarget </div>
.closest(".group") <span className="group-[.show-value]:hidden">
?.classList.add("show-value"); {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
}} <button
onKeyDown={(e) => { type="button"
if (e.key === "Enter" || e.key === " ") { className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget e.currentTarget
.closest(".group") .closest(".group")
?.classList.add("show-value"); ?.classList.add("show-value");
} }}
}} onKeyDown={(e) => {
> if (e.key === "Enter" || e.key === " ") {
<FontAwesomeIcon icon={faEye} /> e.currentTarget
</button> .closest(".group")
</span> ?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>