mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
requested changes
This commit is contained in:
@@ -23,12 +23,6 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
|
||||
subjectFields?: SecretSubjectFields
|
||||
) {
|
||||
try {
|
||||
if (subjectFields) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
|
||||
}
|
||||
} catch {
|
||||
if (subjectFields) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
@@ -40,6 +34,12 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
|
||||
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({
|
||||
method: "GET",
|
||||
url: "/secrets-by-keys",
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
TCreateSecretDTO,
|
||||
TDeleteManySecretDTO,
|
||||
TDeleteSecretDTO,
|
||||
TGetAccessibleSecretsDTO,
|
||||
TGetASecretDTO,
|
||||
TGetSecretReferencesTreeDTO,
|
||||
TGetSecretsDTO,
|
||||
@@ -200,7 +201,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
|
||||
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
|
||||
references.forEach((el) => {
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
secretName: el.secretKey,
|
||||
@@ -1291,7 +1292,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
|
||||
expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined,
|
||||
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
|
||||
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
environment: expandEnvironment,
|
||||
@@ -1347,7 +1348,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
let secretValue = secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: "";
|
||||
if (shouldExpandSecretReferences && secretValue) {
|
||||
if (shouldExpandSecretReferences && secretValue && viewSecretValue) {
|
||||
// eslint-disable-next-line
|
||||
const expandedSecretValue = await expandSecretReferences({
|
||||
environment,
|
||||
@@ -2613,6 +2614,86 @@ export const secretV2BridgeServiceFactory = ({
|
||||
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 {
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
@@ -2630,6 +2711,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretsMultiEnv,
|
||||
getSecretReferenceTree,
|
||||
getSecretsByFolderMappings,
|
||||
getSecretById
|
||||
getSecretById,
|
||||
getAccessibleSecrets
|
||||
};
|
||||
};
|
||||
|
||||
@@ -351,3 +351,10 @@ export type TGetSecretsRawByFolderMappingsDTO = {
|
||||
filters: TFindSecretsByFolderIdsFilter;
|
||||
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||
};
|
||||
|
||||
export type TGetAccessibleSecretsDTO = {
|
||||
environment: string;
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -81,6 +81,7 @@ import {
|
||||
TDeleteManySecretRawDTO,
|
||||
TDeleteSecretDTO,
|
||||
TDeleteSecretRawDTO,
|
||||
TGetAccessibleSecretsDTO,
|
||||
TGetASecretByIdRawDTO,
|
||||
TGetASecretDTO,
|
||||
TGetASecretRawDTO,
|
||||
@@ -1312,6 +1313,39 @@ export const secretServiceFactory = ({
|
||||
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 ({
|
||||
projectId,
|
||||
path,
|
||||
@@ -3261,6 +3295,7 @@ export const secretServiceFactory = ({
|
||||
getSecretReferenceTree,
|
||||
getSecretsRawByFolderMappings,
|
||||
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 { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-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">;
|
||||
|
||||
@@ -180,6 +181,12 @@ export enum SecretsOrderBy {
|
||||
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 = {
|
||||
expandSecretReferences?: boolean;
|
||||
path: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
useGetAccessibleSecrets,
|
||||
useGetProjectSecretsDetails,
|
||||
useGetProjectSecretsOverview,
|
||||
useGetProjectSecretsQuickSearch
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DashboardSecretsOrderBy,
|
||||
TDashboardProjectSecretsQuickSearch,
|
||||
TDashboardProjectSecretsQuickSearchResponse,
|
||||
TGetAccessibleSecretsDTO,
|
||||
TGetDashboardProjectSecretsByKeys,
|
||||
TGetDashboardProjectSecretsDetailsDTO,
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
@@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
||||
import { groupBy, unique } from "@app/lib/fn/array";
|
||||
|
||||
import { SecretV3Raw } from "../types";
|
||||
|
||||
export const dashboardKeys = {
|
||||
all: () => ["dashboard"] as const,
|
||||
getDashboardSecrets: ({
|
||||
@@ -58,7 +61,14 @@ export const dashboardKeys = {
|
||||
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
|
||||
"quick-search",
|
||||
params
|
||||
] as const
|
||||
] as const,
|
||||
getAccessibleSecrets: ({
|
||||
projectId,
|
||||
secretPath,
|
||||
environment,
|
||||
filterByAction
|
||||
}: TGetAccessibleSecretsDTO) =>
|
||||
[...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const
|
||||
};
|
||||
|
||||
export const fetchProjectSecretsOverview = async ({
|
||||
@@ -295,6 +305,22 @@ export const fetchProjectSecretsQuickSearch = async ({
|
||||
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 = (
|
||||
{
|
||||
projectId,
|
||||
@@ -357,3 +383,32 @@ export const useGetProjectSecretsQuickSearch = (
|
||||
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 })
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||
@@ -101,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = {
|
||||
environment: 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";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
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 { SecretType } from "@app/hooks/api/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string | null;
|
||||
@@ -79,6 +81,9 @@ export const SecretEditRow = ({
|
||||
value: defaultValue || null
|
||||
}
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
@@ -120,6 +125,11 @@ export const SecretEditRow = ({
|
||||
reset({ value });
|
||||
};
|
||||
|
||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue
|
||||
);
|
||||
|
||||
const handleDeleteSecret = useCallback(async () => {
|
||||
setIsDeleting.on();
|
||||
setIsModalOpen(false);
|
||||
@@ -229,48 +239,43 @@ export const SecretEditRow = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip
|
||||
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
|
||||
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(defaultValue || "")
|
||||
? "Secret Reference Tree"
|
||||
: "Secret does not contain references"
|
||||
}
|
||||
>
|
||||
<SecretReferenceTree
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
secretKey={secretName}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="reference-tree"
|
||||
className="h-full"
|
||||
isDisabled={!hasSecretReference(defaultValue || "") || !canReadSecretValue}
|
||||
>
|
||||
<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
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
secretKey={secretName}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||
import { useGetAccessibleSecrets } from "@app/hooks/api/dashboard";
|
||||
|
||||
const formSchema = z.object({
|
||||
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
|
||||
),
|
||||
secrets: z
|
||||
.object({ key: z.string(), value: z.string().optional() })
|
||||
.object({ secretKey: z.string(), secretValue: z.string().optional() })
|
||||
.array()
|
||||
.min(1, "Select one or more secrets to copy")
|
||||
});
|
||||
@@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({
|
||||
const selectedEnvSlug = watch("environment");
|
||||
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
|
||||
|
||||
const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug.slug,
|
||||
secretPath: debouncedEnvCopySecretPath,
|
||||
options: {
|
||||
enabled:
|
||||
Boolean(workspaceId) &&
|
||||
Boolean(selectedEnvSlug) &&
|
||||
Boolean(debouncedEnvCopySecretPath) &&
|
||||
isOpen
|
||||
}
|
||||
});
|
||||
const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } =
|
||||
useGetAccessibleSecrets({
|
||||
projectId: workspaceId,
|
||||
secretPath: debouncedEnvCopySecretPath,
|
||||
environment: selectedEnvSlug.slug,
|
||||
filterByAction: shouldIncludeValues
|
||||
? ProjectPermissionSecretActions.ReadValue
|
||||
: ProjectPermissionSecretActions.DescribeSecret,
|
||||
options: {
|
||||
enabled:
|
||||
Boolean(workspaceId) &&
|
||||
Boolean(selectedEnvSlug) &&
|
||||
Boolean(debouncedEnvCopySecretPath) &&
|
||||
isOpen
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setValue("secrets", []);
|
||||
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
|
||||
|
||||
const handleSecSelectAll = () => {
|
||||
if (secrets) {
|
||||
setValue("secrets", secrets, { shouldDirty: true });
|
||||
if (accessibleSecrets) {
|
||||
setValue("secrets", accessibleSecrets, { shouldDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||
data.secrets.forEach(({ key, value }) => {
|
||||
secretsToBePulled[key] = {
|
||||
value: (shouldIncludeValues && value) || "",
|
||||
data.secrets.forEach(({ secretKey, secretValue }) => {
|
||||
secretsToBePulled[secretKey] = {
|
||||
value: (shouldIncludeValues && secretValue) || "",
|
||||
comments: [""]
|
||||
};
|
||||
});
|
||||
@@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({
|
||||
<FilterableSelect
|
||||
placeholder={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
isSecretsLoading
|
||||
isAccessibleSecretsLoading
|
||||
? "Loading secrets..."
|
||||
: secrets?.length
|
||||
: accessibleSecrets?.length
|
||||
? "Select secrets..."
|
||||
: "No secrets found..."
|
||||
}
|
||||
isLoading={isSecretsLoading}
|
||||
options={secrets}
|
||||
isLoading={isAccessibleSecretsLoading}
|
||||
options={accessibleSecrets}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti
|
||||
getOptionValue={(option) => option.key}
|
||||
getOptionLabel={(option) => option.key}
|
||||
getOptionValue={(option) => option.secretKey}
|
||||
getOptionLabel={(option) => option.secretKey}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({
|
||||
<Switch
|
||||
id="populate-include-value"
|
||||
isChecked={shouldIncludeValues}
|
||||
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)}
|
||||
onCheckedChange={(isChecked) => {
|
||||
setValue("secrets", []);
|
||||
setShouldIncludeValues(isChecked as boolean);
|
||||
}}
|
||||
>
|
||||
Include secret values
|
||||
</Switch>
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
faShare,
|
||||
faTag,
|
||||
faTrash,
|
||||
faUser,
|
||||
faTriangleExclamation
|
||||
faTriangleExclamation,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -728,8 +728,8 @@ export const SecretDetailSidebar = ({
|
||||
<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, secretValueHidden, actor }, index) => (
|
||||
<div key={`secret-version-${index + 1}`} className="flex flex-row">
|
||||
({ createdAt, secretValue, secretValueHidden, 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">
|
||||
@@ -739,72 +739,50 @@ export const SecretDetailSidebar = ({
|
||||
</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 w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<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"
|
||||
<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 w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<Tooltip
|
||||
content={getModifiedByName(actor.actorType, actor.name)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={getModifiedByIcon(actor.actorType)}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/* 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
|
||||
icon={getModifiedByIcon(actor.actorType)}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<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 text-left"
|
||||
onClick={(e) => {
|
||||
if (secretValueHidden) return;
|
||||
)}
|
||||
<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 text-left"
|
||||
onClick={(e) => {
|
||||
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 || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
@@ -822,13 +800,30 @@ export const SecretDetailSidebar = ({
|
||||
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}
|
||||
}}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
@@ -837,50 +832,50 @@ export const SecretDetailSidebar = ({
|
||||
>
|
||||
{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");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
</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">
|
||||
{secretValueHidden ? "******" : 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>
|
||||
|
||||
Reference in New Issue
Block a user