From 4955e2064d1052596b93c96d51f3e7c040f56f6f Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 21 Mar 2025 10:10:35 -0300 Subject: [PATCH 01/40] Add replicate folder content functionality --- .../src/server/routes/v1/dashboard-router.ts | 6 +- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 4 +- .../secret-v2-bridge-service.ts | 43 +- .../secret-v2-bridge-types.ts | 1 + backend/src/services/secret/secret-service.ts | 6 +- backend/src/services/secret/secret-types.ts | 1 + frontend/src/hooks/api/dashboard/queries.tsx | 14 +- frontend/src/hooks/api/dashboard/types.ts | 1 + frontend/src/hooks/api/secrets/types.ts | 1 + .../components/ActionBar/ActionBar.tsx | 385 +++++++++++++++++- .../ReplicateFolderFromBoard.tsx | 307 ++++++++++++++ .../SecretTreeView.tsx | 320 +++++++++++++++ 12 files changed, 1069 insertions(+), 20 deletions(-) create mode 100644 frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx create mode 100644 frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index db61594a2..c8c5275d7 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -845,6 +845,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + recursive: booleanSchema.default(false), filterByAction: z .enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue]) .default(ProjectPermissionSecretActions.ReadValue) @@ -863,7 +864,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { projectId, environment, secretPath, filterByAction } = req.query; + const { projectId, environment, secretPath, filterByAction, recursive } = req.query; const { secrets } = await server.services.secret.getAccessibleSecrets({ actorId: req.permission.id, @@ -873,7 +874,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { environment, secretPath, projectId, - filterByAction + filterByAction, + recursive }); return { secrets }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 0abf02aaa..a5b47ca57 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -356,7 +356,7 @@ export const fnSecretBulkDelete = async ({ interface FolderMap { [parentId: string]: TSecretFolders[]; } -const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { +export const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { const map: FolderMap = {}; map.null = []; // Initialize mapping for root directory @@ -371,7 +371,7 @@ const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { return map; }; -const generatePaths = ( +export const generatePaths = ( map: FolderMap, parentId: string = "null", basePath: string = "", diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index dc975854f..6fdfb10f1 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -44,10 +44,12 @@ import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { + buildHierarchy, expandSecretReferencesFactory, fnSecretBulkDelete, fnSecretBulkInsert, fnSecretBulkUpdate, + generatePaths, getAllSecretReferences, recursivelyGetSecretPaths, reshapeBridgeSecret @@ -2620,7 +2622,8 @@ export const secretV2BridgeServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + recursive }: TGetAccessibleSecretsDTO) => { const { permission } = await permissionService.getProjectPermission({ actor, @@ -2635,10 +2638,38 @@ export const secretV2BridgeServiceFactory = ({ secretPath }); + const folders = []; const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) return { secrets: [] }; + folders.push({ ...folder, parentId: null }); - const secrets = await secretDAL.findByFolderIds([folder.id]); + const env = await projectEnvDAL.findOne({ + projectId, + slug: environment + }); + + if (!env) { + throw new NotFoundError({ + message: `Environment with slug '${environment}' in project with ID ${projectId} not found` + }); + } + + if (recursive) { + const subFolders = await folderDAL.find({ + envId: env.id, + isReserved: false + }); + folders.push(...subFolders); + } + + if (folders.length === 0) return { secrets: [] }; + + const folderMap = buildHierarchy(folders); + const paths = Object.fromEntries( + generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)]) + ); + + const secrets = await secretDAL.findByFolderIds(folders.map((f) => f.id)); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, @@ -2650,7 +2681,7 @@ export const secretV2BridgeServiceFactory = ({ if ( !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, - secretPath, + secretPath: paths[el.folderId], secretName: el.key, secretTags: el.tags.map((i) => i.slug) }) @@ -2661,7 +2692,7 @@ export const secretV2BridgeServiceFactory = ({ if (filterByAction === ProjectPermissionSecretActions.ReadValue) { return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, - secretPath, + secretPath: paths[el.folderId], secretName: el.key, secretTags: el.tags.map((i) => i.slug) }); @@ -2674,7 +2705,7 @@ export const secretV2BridgeServiceFactory = ({ filterByAction === ProjectPermissionSecretActions.DescribeSecret && !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, - secretPath, + secretPath: paths[secret.folderId], secretName: secret.key, secretTags: secret.tags.map((i) => i.slug) }); @@ -2682,7 +2713,7 @@ export const secretV2BridgeServiceFactory = ({ return reshapeBridgeSecret( projectId, environment, - secretPath, + paths[secret.folderId], { ...secret, value: secret.encryptedValue diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 7f415bff8..5c5b26ae4 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -356,5 +356,6 @@ export type TGetAccessibleSecretsDTO = { environment: string; projectId: string; secretPath: string; + recursive?: boolean; filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; } & TProjectPermission; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 13a5c9736..7aeb59f01 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1321,7 +1321,8 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, environment, - filterByAction + filterByAction, + recursive }: TGetAccessibleSecretsDTO) => { const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1340,7 +1341,8 @@ export const secretServiceFactory = ({ actor, actorId, actorOrgId, - actorAuthMethod + actorAuthMethod, + recursive }); return secrets; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 6a5c097ed..bea59a439 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -184,6 +184,7 @@ export enum SecretsOrderBy { export type TGetAccessibleSecretsDTO = { secretPath: string; environment: string; + recursive?: boolean; filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; } & TProjectPermission; diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 1fb456325..c4b89dc90 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -313,12 +313,13 @@ const fetchAccessibleSecrets = async ({ projectId, secretPath, environment, - filterByAction + filterByAction, + recursive = false }: TGetAccessibleSecretsDTO) => { const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>( "/api/v1/dashboard/accessible-secrets", { - params: { projectId, secretPath, environment, filterByAction } + params: { projectId, secretPath, environment, filterByAction, recursive } } ); @@ -393,7 +394,8 @@ export const useGetAccessibleSecrets = ({ secretPath, environment, filterByAction, - options + options, + recursive = false }: TGetAccessibleSecretsDTO & { options?: Omit< UseQueryOptions< @@ -411,8 +413,10 @@ export const useGetAccessibleSecrets = ({ projectId, secretPath, environment, - filterByAction + filterByAction, + recursive }), - queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction }) + queryFn: () => + fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive }) }); }; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 9540c4ae6..4092c33fc 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -107,6 +107,7 @@ export type TGetAccessibleSecretsDTO = { projectId: string; secretPath: string; environment: string; + recursive?: boolean; filterByAction: | ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 5c4d4ffc6..58383e1ff 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -121,6 +121,7 @@ export type TGetProjectSecretsKey = { includeImports?: boolean; viewSecretValue?: boolean; expandSecretReferences?: boolean; + recursive?: boolean; }; export type TGetProjectSecretsDTO = TGetProjectSecretsKey; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 681ad99e3..e8dc7df75 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -17,10 +17,12 @@ import { faKey, faLock, faMinusSquare, + faPaste, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import FileSaver from "file-saver"; import { twMerge } from "tailwind-merge"; @@ -53,8 +55,19 @@ import { useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; -import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; +import { + useCreateFolder, + useCreateSecretBatch, + useDeleteSecretBatch, + useMoveSecrets, + useUpdateSecretBatch +} from "@app/hooks/api"; +import { + dashboardKeys, + fetchDashboardProjectSecretsByKeys +} from "@app/hooks/api/dashboard/queries"; +import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; +import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries"; import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; @@ -65,11 +78,19 @@ import { useSelectedSecrets } from "../../SecretMainPage.store"; import { Filter, RowType } from "../../SecretMainPage.types"; +import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard"; import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm"; import { CreateSecretImportForm } from "./CreateSecretImportForm"; import { FolderForm } from "./FolderForm"; import { MoveSecretsModal } from "./MoveSecretsModal"; +type TParsedEnv = Record; +type TParsedFolderEnv = Record< + string, + Record +>; +type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; + type Props = { // switch the secrets type as it gets decrypted after api call environment: string; @@ -114,7 +135,9 @@ export const ActionBar = ({ "bulkDeleteSecrets", "moveSecrets", "misc", - "upgradePlan" + "upgradePlan", + "replicateFolder", + "confirmUpload" ] as const); const isProtectedBranch = Boolean(protectedBranchPolicyName); const { subscription } = useSubscription(); @@ -122,6 +145,13 @@ export const ActionBar = ({ const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); const { mutateAsync: moveSecrets } = useMoveSecrets(); + const { mutateAsync: updateSecretBatch, isPending: isUpdatingSecrets } = useUpdateSecretBatch({ + options: { onSuccess: undefined } + }); + const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({ + options: { onSuccess: undefined } + }); + const queryClient = useQueryClient(); const selectedSecrets = useSelectedSecrets(); const { reset: resetSelectedSecret } = useSelectedSecretActions(); @@ -293,6 +323,271 @@ export const ActionBar = ({ } }; + // Replicate Folder Logic + const createSecretCount = Object.keys( + (popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {} + ).length; + + const updateSecretCount = Object.keys( + (popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {} + ).length; + + const isNonConflictingUpload = !updateSecretCount; + const isSubmitting = isCreatingSecrets || isUpdatingSecrets; + + const handleParsedEnvMultiFolder = async (envByPath: TParsedFolderEnv) => { + if (Object.keys(envByPath).length === 0) { + createNotification({ + type: "error", + text: "Failed to find secrets" + }); + return; + } + + try { + const allUpdateSecrets: TParsedEnv = {}; + const allCreateSecrets: TParsedEnv = {}; + + await Promise.all( + Object.entries(envByPath).map(async ([folderPath, secrets]) => { + // Normalize the path + let normalizedPath = folderPath; + + // If the path is "/", use the current secretPath + if (normalizedPath === "/") { + normalizedPath = secretPath; + } else { + // Otherwise, concatenate with the current secretPath, avoiding double slashes + const baseSecretPath = secretPath.endsWith("/") ? secretPath.slice(0, -1) : secretPath; + // Remove leading slash from folder path if present to avoid double slashes + const cleanFolderPath = folderPath.startsWith("/") + ? folderPath.substring(1) + : folderPath; + normalizedPath = `${baseSecretPath}/${cleanFolderPath}`; + } + + const secretFolderKeys = Object.keys(secrets); + + if (secretFolderKeys.length === 0) return; + + // Check which secrets already exist in this path + const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({ + secretPath: normalizedPath, + environment, + projectId: workspaceId, + keys: secretFolderKeys + }); + + // Create a quick lookup for existing secrets + const existingSecretLookup = existingSecrets.reduce>( + (lookup, secret) => ({ ...lookup, [secret.secretKey]: true }), + {} + ); + + // Categorize each secret as update or create + secretFolderKeys.forEach((secretKey) => { + const secretData = secrets[secretKey]; + + // Store the path with the secret for later batch processing + const secretWithPath = { + ...secretData, + secretPath: normalizedPath + }; + + if (existingSecretLookup[secretKey]) { + allUpdateSecrets[secretKey] = secretWithPath; + } else { + allCreateSecrets[secretKey] = secretWithPath; + } + }); + }) + ); + + handlePopUpOpen("confirmUpload", { + update: allUpdateSecrets, + create: allCreateSecrets + }); + } catch (e) { + console.error(e); + createNotification({ + text: "Failed to check for secret conflicts", + type: "error" + }); + handlePopUpClose("confirmUpload"); + } + }; + + const handleSaveFolderImport = async () => { + const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt; + try { + // Group secrets by their path for batch operations + const groupedCreateSecrets: Record< + string, + Array<{ + type: SecretType; + secretComment: string; + secretValue: string; + secretKey: string; + }> + > = {}; + + const groupedUpdateSecrets: Record< + string, + Array<{ + type: SecretType; + secretComment: string; + secretValue: string; + secretKey: string; + }> + > = {}; + + // Collect all unique paths that need folders to be created + const allPaths = new Set(); + + // Add paths from create secrets + Object.values(create || {}).forEach((secData) => { + if (secData.secretPath && secData.secretPath !== secretPath) { + allPaths.add(secData.secretPath); + } + }); + + // Create a map of folder paths to their folder name (last segment) + const folderPaths = Array.from(allPaths).map((path) => { + // Remove trailing slash if it exists + const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path; + // Split by '/' to get path segments + const segments = normalizedPath.split("/"); + // Get the last segment as the folder name + const folderName = segments[segments.length - 1]; + // Get the parent path (everything except the last segment) + const parentPath = segments.slice(0, -1).join("/"); + + return { + folderName, + fullPath: normalizedPath, + parentPath: parentPath || "/" + }; + }); + + // Sort paths by depth (shortest first) to ensure parent folders are created before children + folderPaths.sort( + (a, b) => (a.fullPath.match(/\//g) || []).length - (b.fullPath.match(/\//g) || []).length + ); + + // Track created folders to avoid duplicates + const createdFolders = new Set(); + + // Create all necessary folders in order using Promise.all and reduce + await folderPaths.reduce(async (previousPromise, { folderName, fullPath, parentPath }) => { + // Wait for the previous promise to complete + await previousPromise; + + // Skip if we've already created this folder + if (createdFolders.has(fullPath)) return Promise.resolve(); + + try { + await createFolder({ + name: folderName, + path: parentPath, + environment, + projectId: workspaceId + }); + + createdFolders.add(fullPath); + } catch (err) { + console.log(`Folder ${folderName} may already exist:`, err); + } + + return Promise.resolve(); + }, Promise.resolve()); + + if (Object.keys(create || {}).length > 0) { + Object.entries(create).forEach(([secretKey, secData]) => { + // Use the stored secretPath or fall back to the current secretPath + const path = secData.secretPath || secretPath; + + if (!groupedCreateSecrets[path]) { + groupedCreateSecrets[path] = []; + } + + groupedCreateSecrets[path].push({ + type: SecretType.Shared, + secretComment: secData.comments.join("\n"), + secretValue: secData.value, + secretKey + }); + }); + + await Promise.all( + Object.entries(groupedCreateSecrets).map(([path, secrets]) => + createSecretBatch({ + secretPath: path, + workspaceId, + environment, + secrets + }) + ) + ); + } + + if (Object.keys(update || {}).length > 0) { + Object.entries(update).forEach(([secretKey, secData]) => { + // Use the stored secretPath or fall back to the current secretPath + const path = secData.secretPath || secretPath; + + if (!groupedUpdateSecrets[path]) { + groupedUpdateSecrets[path] = []; + } + + groupedUpdateSecrets[path].push({ + type: SecretType.Shared, + secretComment: secData.comments.join("\n"), + secretValue: secData.value, + secretKey + }); + }); + + // Update secrets for each path in parallel + await Promise.all( + Object.entries(groupedUpdateSecrets).map(([path, secrets]) => + updateSecretBatch({ + secretPath: path, + workspaceId, + environment, + secrets + }) + ) + ); + } + + // Invalidate appropriate queries to refresh UI + queryClient.invalidateQueries({ + queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretApprovalRequestKeys.count({ workspaceId }) + }); + + // Close the modal and show notification + handlePopUpClose("confirmUpload"); + createNotification({ + type: "success", + text: isProtectedBranch + ? "Uploaded changes have been sent for review" + : "Successfully uploaded secrets" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to upload secrets" + }); + } + }; + return ( <>
@@ -570,6 +865,29 @@ export const ActionBar = ({ )} + + {(isAllowed) => ( + + )} +
@@ -679,6 +997,15 @@ export const ActionBar = ({ handlePopUpToggle={handlePopUpToggle} onMoveApproved={handleSecretsMove} /> + handlePopUpToggle("replicateFolder", isOpen)} + onParsedEnv={handleParsedEnvMultiFolder} + environment={environment} + environments={currentWorkspace.environments} + workspaceId={workspaceId} + secretPath={secretPath} + /> {subscription && ( )} + handlePopUpToggle("confirmUpload", open)} + > + + {isNonConflictingUpload ? "Upload" : "Overwrite"} + , + + ]} + > + {isNonConflictingUpload ? ( +
+ Are you sure you want to import {createSecretCount} secret + {createSecretCount > 1 ? "s" : ""} to this environment? +
+ ) : ( +
+
Your project already contains the following {updateSecretCount} secrets:
+
+ {Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {}) + ?.map((key) => key) + .join(", ")} +
+
+ Are you sure you want to overwrite these secrets + {createSecretCount > 0 + ? ` and import ${createSecretCount} new + one${createSecretCount > 1 ? "s" : ""}` + : ""} + ? +
+
+ )} +
+
); }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx new file mode 100644 index 000000000..ba17d59cd --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx @@ -0,0 +1,307 @@ +import { useEffect, useMemo, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faClone } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FilterableSelect, + FormControl, + Modal, + ModalContent, + Switch +} from "@app/components/v2"; +import { SecretPathInput } from "@app/components/v2/SecretPathInput"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { useDebounce } from "@app/hooks"; +import { useGetAccessibleSecrets } from "@app/hooks/api/dashboard"; +import { SecretV3Raw } from "@app/hooks/api/types"; + +import { SecretTreeView } from "./SecretTreeView"; + +const formSchema = z.object({ + environment: z.object({ name: z.string(), slug: z.string() }), + secretPath: z + .string() + .trim() + .transform((val) => + typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val + ), + secrets: z + .object({ + secretKey: z.string(), + secretValue: z.string().optional(), + secretPath: z.string() + }) + .array() + .min(1, "Select one or more secrets to copy") +}); + +type TFormSchema = z.infer; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + onParsedEnv: ( + env: Record> + ) => void; + environments?: { name: string; slug: string }[]; + workspaceId: string; + environment: string; + secretPath: string; +}; + +type SecretFolder = { + items: Partial[]; + subFolders: Record; +}; + +type SecretStructure = { + [rootPath: string]: SecretFolder; +}; + +export const ReplicateFolderFromBoard = ({ + environments = [], + workspaceId, + isOpen, + onToggle, + onParsedEnv +}: Props) => { + const [shouldIncludeValues, setShouldIncludeValues] = useState(true); + + const { handleSubmit, control, watch, reset, setValue } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { secretPath: "/", environment: environments?.[0], secrets: [] } + }); + + const envCopySecPath = watch("secretPath"); + const selectedEnvSlug = watch("environment"); + const selectedSecrets = watch("secrets"); + const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath); + + const { data: accessibleSecrets } = useGetAccessibleSecrets({ + projectId: workspaceId, + secretPath: "/", + environment: selectedEnvSlug.slug, + recursive: true, + filterByAction: shouldIncludeValues + ? ProjectPermissionSecretActions.ReadValue + : ProjectPermissionSecretActions.DescribeSecret, + options: { enabled: Boolean(workspaceId) && Boolean(selectedEnvSlug) && isOpen } + }); + + const restructureSecrets = useMemo(() => { + if (!accessibleSecrets) return {}; + + const result: SecretStructure = {}; + result["/"] = { + items: [], + subFolders: {} + }; + + accessibleSecrets.forEach((secret) => { + const path = secret.secretPath; + + if (path === "/") { + result["/"]?.items.push(secret); + return; + } + + const normalizedPath = path.startsWith("/") ? path.substring(1) : path; + const pathParts = normalizedPath.split("/"); + + let currentFolder = result["/"]; + + for (let i = 0; i < pathParts.length; i += 1) { + const part = pathParts[i]; + + // eslint-disable-next-line no-continue + if (!part) continue; + + if (i === pathParts.length - 1) { + if (!currentFolder.subFolders[part]) { + currentFolder.subFolders[part] = { + items: [], + subFolders: {} + }; + } + currentFolder.subFolders[part].items.push(secret); + } else { + if (!currentFolder.subFolders[part]) { + currentFolder.subFolders[part] = { + items: [], + subFolders: {} + }; + } + currentFolder = currentFolder.subFolders[part]; + } + } + }); + + return result; + }, [accessibleSecrets, selectedEnvSlug]); + + const secretsFilteredByPath = useMemo(() => { + let normalizedPath = debouncedEnvCopySecretPath; + normalizedPath = debouncedEnvCopySecretPath.startsWith("/") + ? debouncedEnvCopySecretPath + : `/${debouncedEnvCopySecretPath}`; + if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) { + normalizedPath = debouncedEnvCopySecretPath.slice(0, -1); + } + + if (normalizedPath === "/") { + return restructureSecrets["/"]; + } + + const segments = normalizedPath.split("/").filter((segment) => segment !== ""); + + let currentLevel = restructureSecrets["/"]; + let result = null; + let currentPath = ""; + + if (!currentLevel) { + setValue("secretPath", "/"); + return null; + } + + for (let i = 0; i < segments.length; i += 1) { + const segment = segments[i]; + currentPath += `/${segment}`; + + if (currentLevel.subFolders && currentLevel.subFolders[segment]) { + currentLevel = currentLevel.subFolders[segment]; + + if (currentPath === normalizedPath) { + result = currentLevel; + break; + } + } else { + return null; + } + } + + return result; + }, [restructureSecrets, debouncedEnvCopySecretPath]); + + useEffect(() => { + setValue("secrets", []); + }, [debouncedEnvCopySecretPath, selectedEnvSlug]); + + const handleFormSubmit = async (data: TFormSchema) => { + const secretsToBePulled: Record< + string, + Record + > = {}; + data.secrets.forEach(({ secretKey, secretValue, secretPath: secretPathToRecreate }) => { + const normalizedPath = secretPathToRecreate.startsWith(envCopySecPath) + ? secretPathToRecreate.slice(envCopySecPath.length) + : secretPathToRecreate; + + if (!secretsToBePulled[normalizedPath]) { + secretsToBePulled[normalizedPath] = {}; + } + + secretsToBePulled[normalizedPath][secretKey] = { + value: (shouldIncludeValues && secretValue) || "", + comments: [""], + secretPath: normalizedPath + }; + }); + onParsedEnv(secretsToBePulled); + onToggle(false); + reset(); + }; + + return ( + { + onToggle(state); + reset(); + }} + > + +
+
+ ( + + option.name} + getOptionValue={(option) => option.slug} + /> + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+ { + setValue("secrets", []); + setShouldIncludeValues(isChecked as boolean); + }} + > + Include secret values + +
+
+ + +
+
+
+
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx new file mode 100644 index 000000000..76bed2697 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx @@ -0,0 +1,320 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + faChevronDown, + faChevronRight, + faFolder, + faFolderOpen, + faFolderTree, + faKey +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +import { Checkbox } from "@app/components/v2"; + +interface SecretItem { + id?: string; + secretKey?: string; + secretValue?: string; + secretPath?: string; + [key: string]: any; +} + +interface FolderStructure { + items: SecretItem[]; + subFolders: { + [key: string]: FolderStructure; + }; +} + +interface TreeData { + [key: string]: FolderStructure | null; +} + +interface FolderProps { + name: string; + structure: FolderStructure; + path: string; + selectedItems: SecretItem[]; + onItemSelect: (item: SecretItem, isChecked: boolean) => void; + onFolderSelect: (folderPath: string, isChecked: boolean) => void; + isExpanded?: boolean; + level: number; + basePath?: string; +} + +interface TreeViewProps { + data: FolderStructure | null; + basePath?: string; + className?: string; + onChange: (items: SecretItem[]) => void; +} + +const getAllItemsInFolder = (folder: FolderStructure): SecretItem[] => { + let items: SecretItem[] = []; + + items = items.concat(folder.items); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + Object.entries(folder.subFolders).forEach(([_, subFolder]) => { + items = items.concat(getAllItemsInFolder(subFolder)); + }); + + return items; +}; + +const getDisplayName = (name: string): string => { + const parts = name.split("/"); + return parts[parts.length - 1]; +}; + +const Collapsible = CollapsiblePrimitive.Root; +const CollapsibleTrigger = CollapsiblePrimitive.Trigger; +const CollapsibleContent = CollapsiblePrimitive.Content; + +const Folder: React.FC = ({ + name, + structure, + path, + selectedItems, + onItemSelect, + onFolderSelect, + isExpanded = false, + level, + basePath +}) => { + const [open, setOpen] = useState(isExpanded); + const displayName = useMemo(() => getDisplayName(name), [name]); + + const allItems = useMemo(() => getAllItemsInFolder(structure), [structure]); + const allItemIds = useMemo(() => allItems.map((item) => item.id), [allItems]); + const selectedItemIds = useMemo(() => selectedItems.map((item) => item.id), [selectedItems]); + const allSelected = useMemo( + () => allItemIds.length > 0 && allItemIds.every((id) => selectedItemIds.includes(id)), + [allItemIds, selectedItemIds] + ); + const someSelected = useMemo( + () => allItemIds.some((id) => selectedItemIds.includes(id)) && !allSelected, + [allItemIds, selectedItemIds, allSelected] + ); + const hasContents = structure.items.length > 0 || Object.keys(structure.subFolders).length > 0; + + const handleFolderSelect = (checked: boolean) => { + onFolderSelect(path, checked); + }; + + return ( +
0 ? "4" : 0}`}> + +
+ + + + +
+ 0 ? (open ? faFolderOpen : faFolder) : faFolderTree} + className={`h-4 w-4 text-${level === 0 ? "mineshaft-300" : "yellow"}`} + /> +
+ + + + + {allItemIds.length > 0 && ( + + {allItemIds.length} {allItemIds.length === 1 ? "item" : "items"} + + )} +
+ + +
+ {structure.items.map((item) => ( +
+
+ +
+ onItemSelect(item, !!checked)} + /> + +
+ ))} + + {Object.entries(structure.subFolders).map(([subName, subStructure]) => ( + + ))} +
+
+
+
+ ); +}; + +export const SecretTreeView: React.FC = ({ + data, + basePath = "/", + className = "", + onChange +}) => { + const [selectedItems, setSelectedItems] = useState([]); + const rootPath = "/"; + const treeData: TreeData = data ? { [rootPath]: data as FolderStructure } : { [rootPath]: null }; + + const rootFolders = useMemo(() => { + return Object.entries(treeData); + }, [treeData]); + + const isEmptyData = useMemo(() => { + return ( + !data || (typeof data === "object" && Object.keys(data).length === 0) || !rootFolders.length + ); + }, [data, rootFolders]); + + const handleItemSelect = (item: SecretItem, isChecked: boolean) => { + if (isChecked) { + setSelectedItems((prev) => [...prev, item]); + } else { + setSelectedItems((prev) => prev.filter((i) => i.id !== item.id)); + } + }; + + const handleFolderSelect = (folderPath: string, isChecked: boolean) => { + const getFolderFromPath = (tree: TreeData, path: string): FolderStructure | null => { + if (rootFolders.length === 1 && rootFolders[0][0] === path) { + return rootFolders[0][1]; + } + + let adjustedPath = path; + if (!path.startsWith(rootPath)) { + adjustedPath = rootPath === path ? rootPath : `${rootPath}/${path}`; + } + + if (adjustedPath === "/") return tree["/"]; + + const parts = adjustedPath.split("/").filter((p) => p !== ""); + + let current: any; + current = tree["/"]; + + const targetExists = parts.every((part) => { + if (current?.subFolders && current.subFolders[part]) { + current = current.subFolders[part]; + return true; + } + return false; + }); + + if (!targetExists) { + return null; + } + + return current; + }; + + const folder = getFolderFromPath(treeData, folderPath); + if (!folder) return; + + const folderItems = getAllItemsInFolder(folder); + const folderItemIds = folderItems.map((item) => item.id); + + if (isChecked) { + setSelectedItems((prev) => { + const prevIds = prev.map((item) => item.id); + const newItems = [...prev]; + folderItems.forEach((item) => { + if (!prevIds.includes(item.id)) { + newItems.push(item); + } + }); + return newItems; + }); + } else { + setSelectedItems((prev) => prev.filter((item) => !folderItemIds.includes(item.id))); + } + }; + + useEffect(() => { + setSelectedItems([]); + }, [data]); + + useEffect(() => { + onChange(selectedItems); + }, [selectedItems]); + + return ( +
+
+
+ {isEmptyData ? ( +
+

No secrets or folders available

+
+ ) : ( + rootFolders.map(([folderName, folderStructure]) => ( + + )) + )} +
+ +
+

+ {selectedItems.length} Item{selectedItems.length === 1 ? "" : "s"} Selected +

+
+
+
+ ); +}; From cefcd872ee1df9c446d0fcb7772bcc67ec14b4ae Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 21 Mar 2025 14:46:32 -0300 Subject: [PATCH 02/40] Minor UI improvements --- .../ReplicateFolderFromBoard/SecretTreeView.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx index 76bed2697..824dec1f5 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx @@ -144,14 +144,15 @@ const Folder: React.FC = ({ {allItemIds.length > 0 && ( - + {allItemIds.length} {allItemIds.length === 1 ? "item" : "items"} )} -
+
+
{structure.items.map((item) => (
@@ -309,8 +310,8 @@ export const SecretTreeView: React.FC = ({ )}
-
-

+
+

{selectedItems.length} Item{selectedItems.length === 1 ? "" : "s"} Selected

From 802cf79af596dfefac61040f879b3201f70cf80a Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 25 Mar 2025 08:34:52 -0300 Subject: [PATCH 03/40] Hide copy secrets button and fix corner case for root secret on folder import --- .../ReplicateFolderFromBoard.tsx | 2 +- .../SecretDropzone/SecretDropzone.tsx | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx index ba17d59cd..16eef8769 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx @@ -102,7 +102,7 @@ export const ReplicateFolderFromBoard = ({ }; accessibleSecrets.forEach((secret) => { - const path = secret.secretPath; + const path = secret.secretPath || "/"; if (path === "/") { result["/"]?.items.push(secret); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx index e289c1894..de4043975 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -64,7 +64,8 @@ export const SecretDropzone = ({ const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({ options: { onSuccess: undefined } }); - + // hide copy secrets from board due to import folders feature + const shouldRenderCopySecrets = false; const isSubmitting = isCreatingSecrets || isUpdatingSecrets; const handleDrag = (e: DragEvent) => { @@ -308,16 +309,18 @@ export const SecretDropzone = ({ secretPath={secretPath} isSmaller={isSmaller} /> - handlePopUpToggle("importSecEnv", isOpen)} - onParsedEnv={handleParsedEnv} - environment={environment} - environments={environments} - workspaceId={workspaceId} - secretPath={secretPath} - isSmaller={isSmaller} - /> + {shouldRenderCopySecrets && ( + handlePopUpToggle("importSecEnv", isOpen)} + onParsedEnv={handleParsedEnv} + environment={environment} + environments={environments} + workspaceId={workspaceId} + secretPath={secretPath} + isSmaller={isSmaller} + /> + )} {!isSmaller && ( Date: Tue, 25 Mar 2025 08:45:38 -0300 Subject: [PATCH 04/40] Coderabbit code suggestions --- .../ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx | 2 +- .../ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx index 16eef8769..339612a89 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/ReplicateFolderFromBoard.tsx @@ -171,7 +171,7 @@ export const ReplicateFolderFromBoard = ({ const segment = segments[i]; currentPath += `/${segment}`; - if (currentLevel.subFolders && currentLevel.subFolders[segment]) { + if (currentLevel?.subFolders?.[segment]) { currentLevel = currentLevel.subFolders[segment]; if (currentPath === normalizedPath) { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx index 824dec1f5..4119f54e3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx @@ -128,7 +128,7 @@ const Folder: React.FC = ({ />

= ({
onItemSelect(item, !!checked)} @@ -240,7 +240,7 @@ export const SecretTreeView: React.FC = ({ current = tree["/"]; const targetExists = parts.every((part) => { - if (current?.subFolders && current.subFolders[part]) { + if (current?.subFolders?.[part]) { current = current.subFolders[part]; return true; } From 4654a17e5f6815bc8eb1fad3781f1057bc2fa723 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 26 Mar 2025 05:54:33 +0400 Subject: [PATCH 05/40] feat(audit-logs): filter audit logs by secret key --- .../ee/services/audit-log/audit-log-dal.ts | 22 ++++++++++++++++-- .../services/audit-log/audit-log-service.ts | 1 + .../ee/services/audit-log/audit-log-types.ts | 1 + backend/src/lib/api-docs/constants.ts | 2 ++ .../server/routes/v1/organization-router.ts | 1 + frontend/src/hooks/api/auditLogs/types.tsx | 1 + .../AuditLogsPage/components/LogsFilter.tsx | 23 ++++++++++++++++--- .../AuditLogsPage/components/LogsSection.tsx | 3 +++ .../AuditLogsPage/components/types.tsx | 1 + 9 files changed, 50 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index 21f785835..31916ca1d 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -40,12 +40,14 @@ export const auditLogDALFactory = (db: TDbClient) => { actorId, actorType, secretPath, + secretKey, eventType, eventMetadata }: Omit & { actorId?: string; actorType?: ActorType; secretPath?: string; + secretKey?: string; eventType?: EventType[]; eventMetadata?: Record; }, @@ -90,8 +92,24 @@ export const auditLogDALFactory = (db: TDbClient) => { }); } - if (projectId && secretPath) { - void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]); + if (projectId) { + if (secretPath) { + void sqlQuery.whereRaw(`"eventMetadata"->>'secretPath' = ?`, [secretPath]); + } + if (secretKey) { + void sqlQuery.whereRaw( + `( + "eventMetadata"->>'secretKey' = ? + OR + EXISTS ( + SELECT 1 + FROM jsonb_array_elements("eventMetadata"->'secrets') AS element + WHERE element->>'secretKey' = ? + ) + )`, + [secretKey, secretKey] + ); + } } // Filter by actor type diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index 295923ef5..0802d27e6 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -63,6 +63,7 @@ export const auditLogServiceFactory = ({ actorType: filter.actorType, eventMetadata: filter.eventMetadata, secretPath: filter.secretPath, + secretKey: filter.secretKey, ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index eef11b8a6..2d6337cc7 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -36,6 +36,7 @@ export type TListProjectAuditLogDTO = { auditLogActorId?: string; actorType?: ActorType; secretPath?: string; + secretKey?: string; eventMetadata?: Record; }; } & Omit; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ec855168e..34fb41e00 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -843,6 +843,8 @@ export const AUDIT_LOGS = { eventType: "The type of the event to export.", secretPath: "The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.", + secretKey: + "The key of the secret to query audit logs for. Note that the projectId parameter must also be provided.", userAgentType: "Choose which consuming application to export audit logs for.", eventMetadata: "Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.", diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 21723ef58..ab628972f 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -118,6 +118,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { .optional() .transform((val) => (!val ? val : removeTrailingSlash(val))) .describe(AUDIT_LOGS.EXPORT.secretPath), + secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey), // eventType is split with , for multiple values, we need to transform it to array eventType: z diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 8acaa215a..e69e8a45a 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -11,6 +11,7 @@ export type TGetAuditLogsFilter = { projectId?: string; actor?: string; // user ID format secretPath?: string; + secretKey?: string; startDate?: Date; endDate?: Date; limit: number; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index c32819cde..31db8f916 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -157,7 +157,24 @@ export const LogsFilter = ({ control={control} name="secretPath" render={({ field: { onChange, value, ...field } }) => ( - + + onChange(e.target.value)} + /> + + )} + /> + )} + + {selectedProject?.type === ProjectType.SecretManager && ( + ( + onChange(e.target.value)} /> )} @@ -289,7 +306,7 @@ export const LogsFilter = ({ control={control} render={({ field: { onChange, ...field }, fieldState: { error } }) => { return ( - + { return ( - + (secretPath!, 500); + const [debouncedSecretKey] = useDebounce(secretKey!, 500); return (
@@ -81,6 +83,7 @@ export const LogsSection = withPermission( refetchInterval={refetchInterval} filter={{ secretPath: debouncedSecretPath || undefined, + secretKey: debouncedSecretKey || undefined, eventMetadata: presets?.eventMetadata, projectId, actorType: presets?.actorType, diff --git a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx index 17dcb80fd..ee6909b13 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx @@ -14,6 +14,7 @@ export const auditLogFilterFormSchema = z actor: z.string().optional(), userAgentType: z.nativeEnum(UserAgentType), secretPath: z.string().optional(), + secretKey: z.string().optional(), startDate: z.date().optional(), endDate: z.date().optional(), page: z.coerce.number().optional(), From d5741b4a72cef2ced6b5db86f701b2ba0b47fdfc Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 26 Mar 2025 11:13:12 -0300 Subject: [PATCH 06/40] Add horizontal limit, search bar and change icons of access tree component --- .../secret-folder/secret-folder-fns.ts | 5 +- .../permissions/AccessTree/AccessTree.tsx | 66 +++++-- .../components/PermissionSimulation.tsx | 6 + .../permissions/AccessTree/hooks/index.ts | 168 +++++++++++++++++- .../AccessTree/nodes/ShowMoreButtonNode.tsx | 36 ++++ .../permissions/AccessTree/types/index.ts | 3 +- .../AccessTree/utils/createBaseEdge.ts | 16 +- .../AccessTree/utils/createShowMoreNode.ts | 25 +++ .../AccessTree/utils/positionElements.ts | 57 ++++-- 9 files changed, 339 insertions(+), 43 deletions(-) create mode 100644 frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx create mode 100644 frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts index a3783a1b9..0369c4e11 100644 --- a/backend/src/services/secret-folder/secret-folder-fns.ts +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -1,14 +1,11 @@ import { TSecretFolders } from "@app/db/schemas"; -import { InternalServerError } from "@app/lib/errors"; export const buildFolderPath = ( folder: TSecretFolders, foldersMap: Record, depth: number = 0 ): string => { - if (depth > 20) { - throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" }); - } + if (depth > 20) return; if (!folder.parentId) { return depth === 0 ? "/" : ""; } diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx index 609a39c4b..30705570c 100644 --- a/frontend/src/components/permissions/AccessTree/AccessTree.tsx +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -1,7 +1,9 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { faArrowUpRightFromSquare, + faDownLeftAndUpRightToCenter, + faUpLong, faUpRightAndDownLeftFromCenter, faWindowRestore } from "@fortawesome/free-solid-svg-icons"; @@ -10,6 +12,7 @@ import { Background, BackgroundVariant, ConnectionLineType, + ControlButton, Controls, Node, NodeMouseHandler, @@ -21,8 +24,10 @@ import { import { twMerge } from "tailwind-merge"; import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2"; +import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode"; import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components"; import { BasePermissionEdge } from "./edges"; import { useAccessTree } from "./hooks"; @@ -35,13 +40,29 @@ export type AccessTreeProps = { const EdgeTypes = { base: BasePermissionEdge }; -const NodeTypes = { role: RoleNode, folder: FolderNode }; +const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode }; const AccessTreeContent = ({ permissions }: AccessTreeProps) => { - const accessTreeData = useAccessTree(permissions); - const { edges, nodes, isLoading, viewMode, setViewMode } = accessTreeData; + const [selectedPath, setSelectedPath] = useState("/"); + const accessTreeData = useAccessTree(permissions, selectedPath); + const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData; - const { fitView, getViewport, setCenter } = useReactFlow(); + useEffect(() => { + setSelectedPath("/"); + }, [environment]); + + const { getViewport, setCenter } = useReactFlow(); + + const goToRootNode = useCallback(() => { + const roleNode = nodes.find((node) => node.type === "role"); + if (roleNode) { + setCenter( + roleNode.position.x + (roleNode.width ? roleNode.width / 2 : 0), + roleNode.position.y + (roleNode.height ? roleNode.height / 2 : 0), + { duration: 800, zoom: 1 } + ); + } + }, [nodes, setCenter]); const onNodeClick: NodeMouseHandler = useCallback( (_, node) => { @@ -56,13 +77,9 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { useEffect(() => { setTimeout(() => { - fitView({ - padding: 0.2, - duration: 1000, - maxZoom: 1 - }); + goToRootNode(); }, 1); - }, [fitView, nodes, edges, getViewport()]); + }, [nodes, edges, getViewport()]); const handleToggleModalView = () => setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal)); @@ -133,7 +150,6 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { edges={edges} edgeTypes={EdgeTypes} nodeTypes={NodeTypes} - fitView onNodeClick={onNodeClick} colorMode="dark" nodesDraggable={false} @@ -151,6 +167,12 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { )} {viewMode !== ViewMode.Docked && ( + { @@ -187,9 +209,25 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { )} + {viewMode === ViewMode.Docked && ( + + + + )} - + + + + + + +
diff --git a/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx index 476f2d27c..6e3790b3c 100644 --- a/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx +++ b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx @@ -46,6 +46,12 @@ export const PermissionSimulation = ({ className="mr-1 rounded" colorSchema="secondary" onClick={handlePermissionSimulation} + rightIcon={ + + } > Permission Simulation diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index 5672230c8..9a0292fb6 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -5,6 +5,7 @@ import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react"; import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries"; +import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types"; import { useAccessTreeContext } from "../components"; import { PermissionAccess } from "../types"; @@ -15,8 +16,24 @@ import { getSubjectActionRuleMap, positionElements } from "../utils"; +import { createShowMoreNode } from "../utils/createShowMoreNode"; -export const useAccessTree = (permissions: MongoAbility) => { +const INITIAL_FOLDERS_PER_LEVEL = 10; +const FOLDERS_INCREMENT = 10; + +type LevelFolderMap = Record< + string, + { + folders: TSecretFolderWithPath[]; + visibleCount: number; + hasMore: boolean; + } +>; + +export const useAccessTree = ( + permissions: MongoAbility, + searchPath: string +) => { const { currentWorkspace } = useWorkspace(); const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext(); const [nodes, setNodes] = useNodesState([]); @@ -27,10 +44,104 @@ export const useAccessTree = (permissions: MongoAbility({}); + const [totalFolderCount, setTotalFolderCount] = useState(0); + + const showMoreFolders = (parentId: string) => { + setLevelFolderMap((prevMap) => { + const level = prevMap[parentId]; + if (!level) return prevMap; + + const newVisibleCount = Math.min( + level.visibleCount + FOLDERS_INCREMENT, + level.folders.length + ); + + return { + ...prevMap, + [parentId]: { + ...level, + visibleCount: newVisibleCount, + hasMore: newVisibleCount < level.folders.length + } + }; + }); + }; + + const levelsWithMoreFolders = Object.entries(levelFolderMap) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([_, level]) => level.hasMore) + .map(([parentId]) => parentId); + + const getLevelCounts = (parentId: string) => { + const level = levelFolderMap[parentId]; + if (!level) return { visibleCount: 0, totalCount: 0, hasMore: false }; + + return { + visibleCount: level.visibleCount, + totalCount: level.folders.length, + hasMore: level.hasMore + }; + }; + useEffect(() => { if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return; - const { folders, name } = environmentsFolders[environment]; + const { folders } = environmentsFolders[environment]; + + setTotalFolderCount(folders.length); + + const groupedFolders: Record = {}; + + const filteredFolders = folders.filter((folder) => { + if (folder.path.startsWith(searchPath)) { + return true; + } + + if ( + searchPath.startsWith(folder.path) && + (folder.path === "/" || + searchPath === folder.path || + searchPath.indexOf("/", folder.path.length) === folder.path.length) + ) { + return true; + } + + return false; + }); + + filteredFolders.forEach((folder) => { + const parentId = folder.parentId || ""; + if (!groupedFolders[parentId]) { + groupedFolders[parentId] = []; + } + groupedFolders[parentId].push(folder); + }); + + const newLevelFolderMap: LevelFolderMap = {}; + + Object.entries(groupedFolders).forEach(([parentId, folderList]) => { + const key = parentId; + newLevelFolderMap[key] = { + folders: folderList, + visibleCount: Math.min(INITIAL_FOLDERS_PER_LEVEL, folderList.length), + hasMore: folderList.length > INITIAL_FOLDERS_PER_LEVEL + }; + }); + + setLevelFolderMap(newLevelFolderMap); + }, [permissions, environmentsFolders, environment, subject, secretName, searchPath]); + + useEffect(() => { + if ( + !environmentsFolders || + !permissions || + !environmentsFolders[environment] || + Object.keys(levelFolderMap).length === 0 + ) + return; + + const { name } = environmentsFolders[environment]; const roleNode = createRoleNode({ subject, @@ -39,7 +150,13 @@ export const useAccessTree = (permissions: MongoAbility + const visibleFolders: TSecretFolderWithPath[] = []; + + Object.values(levelFolderMap).forEach((levelData) => { + visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount)); + }); + + const folderNodes = visibleFolders.map((folder) => createFolderNode({ folder, permissions, @@ -69,10 +186,44 @@ export const useAccessTree = (permissions: MongoAbility { + const key = parentId === "null" ? null : parentId; + + if (key && levelData.hasMore) { + const showMoreButtonNode = createShowMoreNode({ + parentId: key, + onClick: () => showMoreFolders(key), + remaining: levelData.folders.length - levelData.visibleCount + }); + + addMoreButtons.push(showMoreButtonNode); + + folderEdges.push( + createBaseEdge({ + source: key, + target: showMoreButtonNode.id, + access: PermissionAccess.Full, + hideEdge: true + }) + ); + } + }); + + const init = positionElements([roleNode, ...folderNodes, ...addMoreButtons], [...folderEdges]); setNodes(init.nodes); setEdges(init.edges); - }, [permissions, environmentsFolders, environment, subject, secretName, setNodes, setEdges]); + }, [ + levelFolderMap, + permissions, + environmentsFolders, + environment, + subject, + secretName, + setNodes, + setEdges + ]); return { nodes, @@ -86,6 +237,11 @@ export const useAccessTree = (permissions: MongoAbility["data"] }) => { + return ( + <> + + + + + + + ); +}; diff --git a/frontend/src/components/permissions/AccessTree/types/index.ts b/frontend/src/components/permissions/AccessTree/types/index.ts index 9536d7f86..c5da34915 100644 --- a/frontend/src/components/permissions/AccessTree/types/index.ts +++ b/frontend/src/components/permissions/AccessTree/types/index.ts @@ -7,7 +7,8 @@ export enum PermissionAccess { export enum PermissionNode { Role = "role", Folder = "folder", - Environment = "environment" + Environment = "environment", + ShowMoreButton = "showMoreButton" } export enum PermissionEdge { diff --git a/frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts b/frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts index cc53360c8..0e4aa0c9d 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts @@ -5,11 +5,13 @@ import { PermissionAccess, PermissionEdge } from "../types"; export const createBaseEdge = ({ source, target, - access + access, + hideEdge = false }: { source: string; target: string; access: PermissionAccess; + hideEdge?: boolean; }) => { const color = access === PermissionAccess.None ? "#707174" : "#ccccce"; return { @@ -17,10 +19,12 @@ export const createBaseEdge = ({ source, target, type: PermissionEdge.Base, - markerEnd: { - type: MarkerType.ArrowClosed, - color - }, - style: { stroke: color } + markerEnd: hideEdge + ? undefined + : { + type: MarkerType.ArrowClosed, + color + }, + style: { stroke: hideEdge ? "transparent" : color } }; }; diff --git a/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts b/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts new file mode 100644 index 000000000..e5758ffc9 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts @@ -0,0 +1,25 @@ +import { PermissionNode } from "../types"; + +export const createShowMoreNode = ({ + parentId, + onClick, + remaining +}: { + parentId: string | null; + onClick: () => void; + remaining: number; +}) => { + const id = `show-more-${parentId || "root"}`; + return { + id, + type: PermissionNode.ShowMoreButton, + position: { x: 0, y: 0 }, + data: { + parentId, + onClick, + remaining + }, + width: 100, + height: 40 + }; +}; diff --git a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts index 523b402d1..8fd7bbbf5 100644 --- a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts +++ b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts @@ -2,27 +2,60 @@ import Dagre from "@dagrejs/dagre"; import { Edge, Node } from "@xyflow/react"; export const positionElements = (nodes: Node[], edges: Edge[]) => { + const regularNodes = nodes.filter((node) => node.type !== "showMoreButton"); + const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton"); + + const nodeMap: Record = {}; + const dagre = new Dagre.graphlib.Graph({ directed: true }) .setDefaultEdgeLabel(() => ({})) .setGraph({ rankdir: "TB" }); edges.forEach((edge) => dagre.setEdge(edge.source, edge.target)); - nodes.forEach((node) => dagre.setNode(node.id, node)); + + regularNodes.forEach((node) => dagre.setNode(node.id, node)); Dagre.layout(dagre, {}); - return { - nodes: nodes.map((node) => { - const { x, y } = dagre.node(node.id); + const positionedNodes = regularNodes.map((node) => { + const { x, y } = dagre.node(node.id); - return { - ...node, - position: { - x: x - (node.width ? node.width / 2 : 0), - y: y - (node.height ? node.height / 2 : 0) - } - }; - }), + const positionedNode = { + ...node, + position: { + x: x - (node.width ? node.width / 2 : 0), + y: y - (node.height ? node.height / 2 : 0) + } + }; + + nodeMap[node.id] = positionedNode; + + return positionedNode; + }); + + const positionedShowMoreNodes = showMoreNodes.map((node) => { + const parentId = node.data.parentId as string; + const { isStart } = node.data; + + const parentNode = nodeMap[parentId] || positionedNodes[0]; + const parentX = parentNode.position.x; + const parentY = parentNode.position.y; + + const parentWidth = parentNode.width || 150; + const buttonWidth = node.width || 100; + + const buttonX = isStart ? parentX - buttonWidth - 20 : parentX + parentWidth + 20; + + return { + ...node, + position: { + x: buttonX, + y: parentY + } + }; + }); + return { + nodes: [...positionedNodes, ...positionedShowMoreNodes], edges }; }; From 95ccd35f61ebc10f912e6628689ad69d57ee8ad6 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 26 Mar 2025 15:19:42 -0300 Subject: [PATCH 07/40] Search bar improvements and position show more on top of last folder of the row --- .../permissions/AccessTree/AccessTree.tsx | 51 +++++--- .../components/AccessTreeSecretPathInput.tsx | 114 ++++++++++++++++++ .../AccessTree/nodes/ShowMoreButtonNode.tsx | 29 ++--- .../AccessTree/utils/positionElements.ts | 44 +++++-- .../v2/SecretPathInput/SecretPathInput.tsx | 2 +- 5 files changed, 198 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx index 30705570c..8376ae72f 100644 --- a/frontend/src/components/permissions/AccessTree/AccessTree.tsx +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -1,9 +1,10 @@ import { useCallback, useEffect, useState } from "react"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { + faArrowsToDot, + faArrowsUpDownLeftRight, faArrowUpRightFromSquare, faDownLeftAndUpRightToCenter, - faUpLong, faUpRightAndDownLeftFromCenter, faWindowRestore } from "@fortawesome/free-solid-svg-icons"; @@ -24,9 +25,9 @@ import { import { twMerge } from "tailwind-merge"; import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2"; -import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput"; import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode"; import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components"; import { BasePermissionEdge } from "./edges"; @@ -46,12 +47,13 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { const [selectedPath, setSelectedPath] = useState("/"); const accessTreeData = useAccessTree(permissions, selectedPath); const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData; + const [initialRender, setInitialRender] = useState(true); useEffect(() => { setSelectedPath("/"); }, [environment]); - const { getViewport, setCenter } = useReactFlow(); + const { getViewport, setCenter, fitView } = useReactFlow(); const goToRootNode = useCallback(() => { const roleNode = nodes.find((node) => node.type === "role"); @@ -76,10 +78,17 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { ); useEffect(() => { - setTimeout(() => { - goToRootNode(); - }, 1); - }, [nodes, edges, getViewport()]); + setInitialRender(true); + }, [selectedPath, environment]); + + useEffect(() => { + if (initialRender) { + setTimeout(() => { + goToRootNode(); + setInitialRender(false); + }, 500); + } + }, [nodes, edges, getViewport(), initialRender]); const handleToggleModalView = () => setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal)); @@ -156,6 +165,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { edgesReconnectable={false} nodesConnectable={false} connectionLineType={ConnectionLineType.SmoothStep} + minZoom={0.001} proOptions={{ hideAttribution: false // we need pro license if we want to hide }} @@ -167,12 +177,14 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { )} {viewMode !== ViewMode.Docked && ( - + {viewMode !== ViewMode.Undocked && ( + + )} { )} {viewMode === ViewMode.Docked && ( - { )} - + + fitView({ duration: 800 })}> + + + + - - + + diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx new file mode 100644 index 000000000..0e3c8777e --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx @@ -0,0 +1,114 @@ +import { useRef, useState } from "react"; +import { faSearch, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { Tooltip } from "@app/components/v2"; +import { SecretPathInput } from "@app/components/v2/SecretPathInput"; + +type AccessTreeSecretPathInputProps = { + placeholder: string; + environment: string; + value: string; + onChange: (path: string) => void; +}; + +export const AccessTreeSecretPathInput = ({ + placeholder, + environment, + value, + onChange +}: AccessTreeSecretPathInputProps) => { + const [isFocused, setIsFocused] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const wrapperRef = useRef(null); + const inputRef = useRef(null); + + const handleFocus = () => { + setIsFocused(true); + }; + + const handleBlur = () => { + setTimeout(() => { + setIsFocused(false); + }, 200); + }; + + const focusInput = () => { + const inputElement = inputRef.current?.querySelector("input"); + if (inputElement) { + inputElement.focus(); + } + }; + + const toggleSearch = () => { + setIsExpanded(!isExpanded); + if (!isExpanded) { + setTimeout(focusInput, 300); + } + }; + + return ( +
+
+ {isExpanded ? ( +
{ + if (e.key === "Enter" || e.key === " ") { + toggleSearch(); + } + }} + > + +
+ ) : ( + +
{ + if (e.key === "Enter" || e.key === " ") { + toggleSearch(); + } + }} + > + +
+
+ )} + +
+
+ +
+
+
+
+ ); +}; diff --git a/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx index 02b559bf2..2a6cabf43 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx @@ -1,35 +1,30 @@ -import { faChevronRight, faFolderClosed } from "@fortawesome/free-solid-svg-icons"; +import { faChevronRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Handle, NodeProps, Position } from "@xyflow/react"; -import { Tooltip } from "@app/components/v2"; +import { Button, Tooltip } from "@app/components/v2"; import { createShowMoreNode } from "../utils/createShowMoreNode"; export const ShowMoreButtonNode = ({ data: { onClick, remaining } }: NodeProps & { data: ReturnType["data"] }) => { + const tooltipText = `${remaining} ${remaining === 1 ? "folder is" : "folders are"} hidden. Click to show ${remaining > 10 ? "10 more" : ""}`; + return ( <> - - + Show More + ); diff --git a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts index 8fd7bbbf5..8b90a752f 100644 --- a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts +++ b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts @@ -6,6 +6,14 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => { const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton"); const nodeMap: Record = {}; + const childrenMap: Record = {}; + + edges.forEach((edge) => { + if (!childrenMap[edge.source]) { + childrenMap[edge.source] = []; + } + childrenMap[edge.source].push(edge.target); + }); const dagre = new Dagre.graphlib.Graph({ directed: true }) .setDefaultEdgeLabel(() => ({})) @@ -33,27 +41,49 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => { return positionedNode; }); + const findLastChildNode = (parentId: string): Node | undefined => { + const childrenIds = childrenMap[parentId] || []; + if (childrenIds.length === 0) return undefined; + + const childNodes = childrenIds.map((id) => nodeMap[id]).filter(Boolean); + + if (childNodes.length === 0) return undefined; + + childNodes.sort((a, b) => { + if (a.position.y === b.position.y) { + return b.position.x - a.position.x; + } + return b.position.y - a.position.y; + }); + + return childNodes[0]; + }; + const positionedShowMoreNodes = showMoreNodes.map((node) => { const parentId = node.data.parentId as string; - const { isStart } = node.data; const parentNode = nodeMap[parentId] || positionedNodes[0]; - const parentX = parentNode.position.x; - const parentY = parentNode.position.y; + const lastChildNode = findLastChildNode(parentId); - const parentWidth = parentNode.width || 150; - const buttonWidth = node.width || 100; + const referenceNode = lastChildNode || parentNode; - const buttonX = isStart ? parentX - buttonWidth - 20 : parentX + parentWidth + 20; + const referenceX = referenceNode.position.x; + const referenceY = referenceNode.position.y; + + const referenceWidth = referenceNode.width || 150; + + const buttonX = referenceX + referenceWidth - 85; + const buttonY = referenceY - 25; return { ...node, position: { x: buttonX, - y: parentY + y: buttonY } }; }); + return { nodes: [...positionedNodes, ...positionedShowMoreNodes], edges diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index 8e82c2915..31d0ff72c 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -141,7 +141,7 @@ export const SecretPathInput = ({ maxHeight: "var(--radix-select-content-available-height)" }} > -
+
{suggestions.map((suggestion, i) => (
Date: Thu, 27 Mar 2025 00:19:26 +0530 Subject: [PATCH 08/40] feat: completed folder last secret commit feature --- ...50326171707_folder-last-secret-modified.ts | 21 +++++++++++++++++++ backend/src/db/schemas/secret-folders.ts | 3 ++- backend/src/lib/api-docs/constants.ts | 3 ++- .../server/routes/v1/secret-folder-router.ts | 1 + .../secret-folder/secret-folder-service.ts | 19 +++++++++++++++-- .../secret-folder/secret-folder-types.ts | 1 + backend/src/services/secret/secret-queue.ts | 4 ++++ 7 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 backend/src/db/migrations/20250326171707_folder-last-secret-modified.ts diff --git a/backend/src/db/migrations/20250326171707_folder-last-secret-modified.ts b/backend/src/db/migrations/20250326171707_folder-last-secret-modified.ts new file mode 100644 index 000000000..227fe6ef7 --- /dev/null +++ b/backend/src/db/migrations/20250326171707_folder-last-secret-modified.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.SecretFolder, "lastSecretModified"); + if (!hasCol) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.datetime("lastSecretModified"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.SecretFolder, "lastSecretModified"); + if (hasCol) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.dropColumn("lastSecretModified"); + }); + } +} diff --git a/backend/src/db/schemas/secret-folders.ts b/backend/src/db/schemas/secret-folders.ts index 33b13b673..09e2fe8c1 100644 --- a/backend/src/db/schemas/secret-folders.ts +++ b/backend/src/db/schemas/secret-folders.ts @@ -16,7 +16,8 @@ export const SecretFoldersSchema = z.object({ envId: z.string().uuid(), parentId: z.string().uuid().nullable().optional(), isReserved: z.boolean().default(false).nullable().optional(), - description: z.string().nullable().optional() + description: z.string().nullable().optional(), + lastSecretModified: z.date().nullable().optional() }); export type TSecretFolders = z.infer; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ec855168e..5acc04d05 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -632,7 +632,8 @@ export const FOLDERS = { environment: "The slug of the environment to list folders from.", path: "The path to list folders from.", directory: "The directory to list folders from. (Deprecated in favor of path)", - recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories." + recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.", + lastSecretModified: "The timestamp filters folders with secrets modified after the specified date." }, GET_BY_ID: { folderId: "The ID of the folder to get details." diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index 5f8b553f5..b55564d80 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -335,6 +335,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => querystring: z.object({ workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId), environment: z.string().trim().describe(FOLDERS.LIST.environment), + lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified), path: z .string() .trim() diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index d4f58a1d4..97cd19755 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -402,7 +402,8 @@ export const secretFolderServiceFactory = ({ orderDirection, limit, offset, - recursive + recursive, + lastSecretModified }: TGetFolderDTO) => { // folder list is allowed to be read by anyone // permission to check does user has access @@ -425,7 +426,16 @@ export const secretFolderServiceFactory = ({ const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] }); // remove the parent folder return recursiveFolders - .filter((folder) => folder.id !== parentFolder.id) + .filter((folder) => { + if (lastSecretModified) { + if (!folder.lastSecretModified) return false; + + if (folder.lastSecretModified < new Date(lastSecretModified)) { + return false; + } + } + return folder.id !== parentFolder.id; + }) .map((folder) => ({ ...folder, relativePath: folder.path @@ -445,6 +455,11 @@ export const secretFolderServiceFactory = ({ offset } ); + if (lastSecretModified) { + return folders.filter((el) => + el.lastSecretModified ? el.lastSecretModified >= new Date(lastSecretModified) : false + ); + } return folders; }; diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index 2a5681b5b..4008676db 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -46,6 +46,7 @@ export type TGetFolderDTO = { limit?: number; offset?: number; recursive?: boolean; + lastSecretModified?: string; } & TProjectPermission; export type TGetFolderByIdDTO = { diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 76a486b46..60eec9cb1 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -646,6 +646,10 @@ export const secretQueueFactory = ({ } ); + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return; + await folderDAL.updateById(folder.id, { lastSecretModified: new Date() }); + await secretSyncQueue.queueSecretSyncsSyncSecretsByPath({ projectId, environmentSlug: environment, secretPath }); await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false }); From a5f198a3d57e54e012a6dd76a15b1b74da9c49e4 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 26 Mar 2025 17:30:20 -0300 Subject: [PATCH 09/40] Allow access tree relative path graph on path filter --- .../permissions/AccessTree/AccessTree.tsx | 4 +- .../permissions/AccessTree/hooks/index.ts | 93 +++++++++++++++---- .../components/AccessTreeSecretPathInput.tsx | 12 ++- .../v2/SecretPathInput/SecretPathInput.tsx | 4 - 4 files changed, 86 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx index 8376ae72f..265e87623 100644 --- a/frontend/src/components/permissions/AccessTree/AccessTree.tsx +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { - faArrowsToDot, + faAnglesUp, faArrowsUpDownLeftRight, faArrowUpRightFromSquare, faDownLeftAndUpRightToCenter, @@ -241,7 +241,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { - + diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index 9a0292fb6..8651ea543 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -88,21 +88,18 @@ export const useAccessTree = ( if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return; const { folders } = environmentsFolders[environment]; - setTotalFolderCount(folders.length); - const groupedFolders: Record = {}; + const searchPathFolder = folders.find((folder) => folder.path === searchPath); const filteredFolders = folders.filter((folder) => { - if (folder.path.startsWith(searchPath)) { + if (folder.path === searchPath) { return true; } if ( - searchPath.startsWith(folder.path) && - (folder.path === "/" || - searchPath === folder.path || - searchPath.indexOf("/", folder.path.length) === folder.path.length) + folder.path.startsWith(searchPath) && + (searchPath === "/" || folder.path.charAt(searchPath.length) === "/") ) { return true; } @@ -110,11 +107,17 @@ export const useAccessTree = ( return false; }); + const rootFolder = searchPathFolder || filteredFolders.find((f) => f.path === "/"); + + const groupedFolders: Record = {}; + filteredFolders.forEach((folder) => { const parentId = folder.parentId || ""; + if (!groupedFolders[parentId]) { groupedFolders[parentId] = []; } + groupedFolders[parentId].push(folder); }); @@ -129,7 +132,18 @@ export const useAccessTree = ( }; }); - setLevelFolderMap(newLevelFolderMap); + if (rootFolder) { + setLevelFolderMap({ + ...newLevelFolderMap, + __rootFolderId: { + folders: [rootFolder], + visibleCount: 1, + hasMore: false + } + }); + } else { + setLevelFolderMap(newLevelFolderMap); + } }, [permissions, environmentsFolders, environment, subject, secretName, searchPath]); useEffect(() => { @@ -151,11 +165,15 @@ export const useAccessTree = ( const actionRuleMap = getSubjectActionRuleMap(subject, permissions); const visibleFolders: TSecretFolderWithPath[] = []; - - Object.values(levelFolderMap).forEach((levelData) => { - visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount)); + Object.entries(levelFolderMap).forEach(([key, levelData]) => { + if (key !== "__rootFolderId") { + visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount)); + } }); + // eslint-disable-next-line no-underscore-dangle + const rootFolder = levelFolderMap.__rootFolderId?.folders[0]; + const folderNodes = visibleFolders.map((folder) => createFolderNode({ folder, @@ -167,10 +185,45 @@ export const useAccessTree = ( }) ); - const folderEdges = folderNodes.map(({ data: folder }) => { - const actions = Object.values(folder.actions); + const folderEdges: Edge[] = []; + if (rootFolder) { + const rootFolderNode = folderNodes.find( + (node) => node.data.id === rootFolder.id || node.data.path === rootFolder.path + ); + + if (rootFolderNode) { + const rootActions = Object.values(rootFolderNode.data.actions); + let rootAccess: PermissionAccess; + + if (Object.values(rootActions).some((action) => action === PermissionAccess.Full)) { + rootAccess = PermissionAccess.Full; + } else if ( + Object.values(rootActions).some((action) => action === PermissionAccess.Partial) + ) { + rootAccess = PermissionAccess.Partial; + } else { + rootAccess = PermissionAccess.None; + } + + folderEdges.push( + createBaseEdge({ + source: roleNode.id, + target: rootFolderNode.id, + access: rootAccess + }) + ); + } + } + + folderNodes.forEach(({ data: folder }) => { + if (rootFolder && (folder.id === rootFolder.id || folder.path === rootFolder.path)) { + return; + } + + const actions = Object.values(folder.actions); let access: PermissionAccess; + if (Object.values(actions).some((action) => action === PermissionAccess.Full)) { access = PermissionAccess.Full; } else if (Object.values(actions).some((action) => action === PermissionAccess.Partial)) { @@ -179,16 +232,20 @@ export const useAccessTree = ( access = PermissionAccess.None; } - return createBaseEdge({ - source: folder.parentId ?? roleNode.id, - target: folder.id, - access - }); + folderEdges.push( + createBaseEdge({ + source: folder.parentId ?? roleNode.id, + target: folder.id, + access + }) + ); }); const addMoreButtons: Node[] = []; Object.entries(levelFolderMap).forEach(([parentId, levelData]) => { + if (parentId === "__rootFolderId") return; + const key = parentId === "null" ? null : parentId; if (key && levelData.hasMore) { diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx index 0e3c8777e..1702d1214 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx @@ -1,5 +1,5 @@ -import { useRef, useState } from "react"; -import { faSearch, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { useEffect, useRef, useState } from "react"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -34,6 +34,12 @@ export const AccessTreeSecretPathInput = ({ }, 200); }; + useEffect(() => { + if (!isFocused) { + setIsExpanded(false); + } + }, [isFocused]); + const focusInput = () => { const inputElement = inputRef.current?.querySelector("input"); if (inputElement) { @@ -69,7 +75,7 @@ export const AccessTreeSecretPathInput = ({ } }} > - +
) : ( diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index 31d0ff72c..c82e492e8 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -48,7 +48,6 @@ export const SecretPathInput = ({ }, [propValue]); useEffect(() => { - // update secret path if input is valid if ( (debouncedInputValue.length > 0 && debouncedInputValue[debouncedInputValue.length - 1] === "/") || @@ -59,7 +58,6 @@ export const SecretPathInput = ({ }, [debouncedInputValue]); useEffect(() => { - // filter suggestions based on matching const searchFragment = debouncedInputValue.split("/").pop() || ""; const filteredSuggestions = folders .filter((suggestionEntry) => @@ -78,7 +76,6 @@ export const SecretPathInput = ({ const validPaths = inputValue.split("/"); validPaths.pop(); - // removed trailing slash const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`; onChange?.(newValue); setInputValue(newValue); @@ -102,7 +99,6 @@ export const SecretPathInput = ({ }; const handleInputChange = (e: any) => { - // propagate event to react-hook-form onChange if (onChange) { onChange(e.target.value); } From fb0b6b00ddd9be054274f07f71d450461a855d3b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 27 Mar 2025 03:51:16 +0400 Subject: [PATCH 10/40] fix: added suggested changes --- .../AuditLogsPage/components/LogsFilter.tsx | 363 +++++++++--------- 1 file changed, 187 insertions(+), 176 deletions(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 31db8f916..af813ec23 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-nested-ternary */ import { useState } from "react"; import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; @@ -116,12 +115,7 @@ export const LogsFilter = ({ const selectedProject = watch("project"); return ( -
+
{isOrgAuditLogs && workspacesInOrg.length > 0 && ( ( - + ( - - onChange(e.target.value)} /> + + onChange(e.target.value)} + /> )} /> )}
-
- ( - - - -
- {selectedEventTypes?.length === 1 - ? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0]) - ?.label - : selectedEventTypes?.length === 0 - ? "All events" - : `${selectedEventTypes?.length} events selected`} - -
-
- -
- {eventTypes && eventTypes.length > 0 ? ( - eventTypes.map((eventType) => { - const isSelected = selectedEventTypes?.includes( - eventType.value as EventType - ); - - return ( - eventTypes.length > 1 && event.preventDefault()} - onClick={() => { - if (selectedEventTypes?.includes(eventType.value as EventType)) { - field.onChange( - selectedEventTypes?.filter((e: string) => e !== eventType.value) - ); - } else { - field.onChange([...(selectedEventTypes || []), eventType.value]); - } - }} - key={`event-type-${eventType.value}`} - icon={ - isSelected ? ( - - ) : ( -
- ) - } - iconPos="left" - className="w-[28.4rem] text-sm" - > - {eventType.label} - - ); - }) - ) : ( -
- )} -
- - - - )} - /> - - {!isPending && data && data.length > 0 && !presets?.actorId && ( +
+
( + name="eventType" + render={({ field }) => ( + + + +
+ {selectedEventTypes?.length === 1 + ? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0]) + ?.label + : selectedEventTypes?.length === 0 + ? "All events" + : `${selectedEventTypes?.length} events selected`} + +
+
+ +
+ {eventTypes && eventTypes.length > 0 ? ( + eventTypes.map((eventType) => { + const isSelected = selectedEventTypes?.includes( + eventType.value as EventType + ); + + return ( + eventTypes.length > 1 && event.preventDefault()} + onClick={() => { + if (selectedEventTypes?.includes(eventType.value as EventType)) { + field.onChange( + selectedEventTypes?.filter((e: string) => e !== eventType.value) + ); + } else { + field.onChange([...(selectedEventTypes || []), eventType.value]); + } + }} + key={`event-type-${eventType.value}`} + icon={ + isSelected ? ( + + ) : ( +
+ ) + } + iconPos="left" + className="w-[28.4rem] text-sm" + > + {eventType.label} + + ); + }) + ) : ( +
+ )} +
+ + + + )} + /> + + {!isPending && data && data.length > 0 && !presets?.actorId && ( + ( + + + + )} + /> + )} + ( )} /> - )} - ( - - - - )} - /> - { - return ( - - - - ); - }} - /> - { - return ( - - - - ); - }} - /> - + { + return ( + + + + ); + }} + /> + { + return ( + + + + ); + }} + /> + +
); From 53c015988df98b8784aab7cf3073d8fb6ed410d2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 27 Mar 2025 09:41:46 +0400 Subject: [PATCH 11/40] feat(audit-logs): filtering revamp --- .../ee/services/audit-log/audit-log-dal.ts | 12 +- .../services/audit-log/audit-log-service.ts | 1 + .../ee/services/audit-log/audit-log-types.ts | 10 + backend/src/lib/api-docs/constants.ts | 2 + .../server/routes/v1/organization-router.ts | 1 + .../src/hooks/api/auditLogs/constants.tsx | 11 + frontend/src/hooks/api/auditLogs/enums.tsx | 4 +- frontend/src/hooks/api/auditLogs/types.tsx | 1 + .../AuditLogsPage/AuditLogsPage.tsx | 2 +- .../components/LogFilterItem.tsx | 32 + .../AuditLogsPage/components/LogsFilter.tsx | 812 +++++++++++------- .../AuditLogsPage/components/LogsSection.tsx | 75 +- .../AuditLogsPage/components/LogsTable.tsx | 6 +- .../AuditLogsPage/components/types.tsx | 12 +- .../UserAuditLogsSection.tsx | 23 - .../IntegrationAuditLogsSection.tsx | 1 - .../components/SecretSyncAuditLogsSection.tsx | 1 - 17 files changed, 615 insertions(+), 391 deletions(-) create mode 100644 frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index 31916ca1d..053ff2d46 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -9,13 +9,14 @@ import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; import { ActorType } from "@app/services/auth/auth-type"; -import { EventType } from "./audit-log-types"; +import { EventType, filterableSecretEvents } from "./audit-log-types"; export type TAuditLogDALFactory = ReturnType; type TFindQuery = { actor?: string; projectId?: string; + environment?: string; orgId?: string; eventType?: string; startDate?: string; @@ -32,6 +33,7 @@ export const auditLogDALFactory = (db: TDbClient) => { { orgId, projectId, + environment, userAgentType, startDate, endDate, @@ -92,7 +94,13 @@ export const auditLogDALFactory = (db: TDbClient) => { }); } - if (projectId) { + const eventIsSecretType = !eventType?.length || eventType.some((event) => filterableSecretEvents.includes(event)); + // We only want to filter for environment/secretPath/secretKey if the user is either checking for all event types + if (projectId && eventIsSecretType) { + if (environment) { + void sqlQuery.whereRaw(`"eventMetadata"->>'environment' = ?`, [environment]); + } + if (secretPath) { void sqlQuery.whereRaw(`"eventMetadata"->>'secretPath' = ?`, [secretPath]); } diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index 0802d27e6..ce6689fe9 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -64,6 +64,7 @@ export const auditLogServiceFactory = ({ eventMetadata: filter.eventMetadata, secretPath: filter.secretPath, secretKey: filter.secretKey, + environment: filter.environment, ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 2d6337cc7..b9a76dbb5 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -287,6 +287,16 @@ export enum EventType { KMIP_OPERATION_REGISTER = "kmip-operation-register" } +export const filterableSecretEvents: EventType[] = [ + EventType.GET_SECRET, + EventType.DELETE_SECRETS, + EventType.CREATE_SECRETS, + EventType.UPDATE_SECRETS, + EventType.CREATE_SECRET, + EventType.UPDATE_SECRET, + EventType.DELETE_SECRET +]; + interface UserActorMetadata { userId: string; email?: string | null; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 34fb41e00..cafbe9269 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -840,6 +840,8 @@ export const AUDIT_LOGS = { EXPORT: { projectId: "Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", + environment: + "The environment to filter logs by. If not provided, logs from all environments will be returned. Note that the projectId parameter must also be provided.", eventType: "The type of the event to export.", secretPath: "The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.", diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index ab628972f..64a5125db 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -112,6 +112,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { description: "Get all audit logs for an organization", querystring: z.object({ projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), + environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment), actorType: z.nativeEnum(ActorType).optional(), secretPath: z .string() diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index d348c7bb1..f4366a6eb 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -1,5 +1,16 @@ import { EventType, UserAgentType } from "./enums"; +export const secretEvents: EventType[] = [ + EventType.GET_SECRETS, + EventType.GET_SECRET, + EventType.DELETE_SECRETS, + EventType.CREATE_SECRETS, + EventType.UPDATE_SECRETS, + EventType.CREATE_SECRET, + EventType.UPDATE_SECRET, + EventType.DELETE_SECRET +]; + export const eventToNameMap: { [K in EventType]: string } = { [EventType.GET_SECRETS]: "List secrets", [EventType.GET_SECRET]: "Read secret", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 76bb283dc..1e010b611 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -12,8 +12,8 @@ export enum UserAgentType { CLI = "cli", K8_OPERATOR = "k8-operator", TERRAFORM = "terraform", - NODE_SDK = "node-sdk", - PYTHON_SDK = "python-sdk", + NODE_SDK = "InfisicalNodeSDK", + PYTHON_SDK = "InfisicalPythonSDK", OTHER = "other" } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index e69e8a45a..a18974f2e 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -9,6 +9,7 @@ export type TGetAuditLogsFilter = { eventMetadata?: Record; actorType?: ActorType; projectId?: string; + environment?: string; actor?: string; // user ID format secretPath?: string; secretKey?: string; diff --git a/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx index e0d2fba73..bec3bfd77 100644 --- a/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx @@ -18,7 +18,7 @@ export const AuditLogsPage = () => { title="Audit logs" description="Audit logs for security and compliance teams to monitor information access." /> - +
diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx new file mode 100644 index 000000000..38df23aad --- /dev/null +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx @@ -0,0 +1,32 @@ +import { twMerge } from "tailwind-merge"; + +import { Button, Tooltip } from "@app/components/v2"; + +type Props = { + hoverTooltip?: string; + className?: string; + label: string; + onClear: () => void; + children: React.ReactNode; +}; + +export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => { + return ( + +
+
+

{label}

+ +
+ {children} +
+
+ ); +}; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index af813ec23..59440c814 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -1,10 +1,26 @@ -import { useState } from "react"; -import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form"; -import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; +/* eslint-disable no-nested-ternary */ +import { useMemo, useState } from "react"; +import { + Control, + Controller, + UseFormGetFieldState, + UseFormReset, + UseFormResetField, + UseFormSetValue, + UseFormWatch +} from "react-hook-form"; +import { + faArrowRight, + faCaretDown, + faCheckCircle, + faFilterCircleXmark +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { + Badge, Button, DatePicker, DropdownMenu, @@ -18,13 +34,17 @@ import { SelectItem } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/api"; -import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; -import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; -import { Actor } from "@app/hooks/api/auditLogs/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; +import { useGetUserWorkspaces } from "@app/hooks/api"; +import { + eventToNameMap, + secretEvents, + userAgentTTypeoNameMap +} from "@app/hooks/api/auditLogs/constants"; +import { EventType } from "@app/hooks/api/auditLogs/enums"; +import { UserAgentType } from "@app/hooks/api/auth/types"; -import { AuditLogFilterFormData } from "./types"; +import { LogFilterItem } from "./LogFilterItem"; +import { AuditLogFilterFormData, Presets } from "./types"; const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value })); const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ @@ -33,26 +53,70 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label })); type Props = { - presets?: { - actorId?: string; - eventType?: EventType[]; - }; - className?: string; - isOrgAuditLogs?: boolean; - setValue: UseFormSetValue; + presets?: Presets; control: Control; reset: UseFormReset; + resetField: UseFormResetField; watch: UseFormWatch; + getFieldState: UseFormGetFieldState; + setValue: UseFormSetValue; +}; + +const getActiveFilterCount = ( + getFieldState: UseFormGetFieldState, + watch: UseFormWatch +) => { + const fields = [ + "actor", + "project", + "eventType", + "startDate", + "endDate", + "environment", + "secretPath", + "userAgentType", + "secretKey" + ] as Partial[]; + + let filterCount = 0; + + // either start or end date should only be counted as one filter + let dateProcessed = false; + + fields.forEach((field) => { + const fieldState = getFieldState(field); + + if ( + field === "userAgentType" || + field === "environment" || + field === "secretKey" || + field === "secretPath" + ) { + const value = watch(field); + + if (value !== undefined && value !== "") { + filterCount += 1; + } + } else if (fieldState.isDirty && !dateProcessed) { + filterCount += 1; + + if (field === "startDate" || field === "endDate") { + dateProcessed = true; + } + } + }); + + return filterCount; }; export const LogsFilter = ({ presets, - isOrgAuditLogs, - className, control, reset, - setValue, - watch + resetField, + watch, + getFieldState, + setValue }: Props) => { const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); @@ -62,317 +126,423 @@ export const LogsFilter = ({ const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id); - const { data, isPending } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? ""); - - const renderActorSelectItem = (actor: Actor) => { - switch (actor.type) { - case ActorType.USER: - return ( - - {actor.metadata.email} - - ); - case ActorType.SERVICE: - return ( - - {actor.metadata.name} - - ); - case ActorType.IDENTITY: - return ( - - {actor.metadata.name} - - ); - case ActorType.KMIP_CLIENT: - return ( - - {actor.metadata.name} - - ); - default: - return ( - - N/A - - ); - } - }; - const selectedEventTypes = watch("eventType") as EventType[] | undefined; const selectedProject = watch("project"); + const showSecretsSection = + selectedEventTypes?.some( + (eventType) => secretEvents.includes(eventType) && eventType !== EventType.GET_SECRETS + ) || selectedEventTypes?.length === 0; + + const availableEnvironments = useMemo(() => { + if (!selectedProject) return []; + + return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? []; + }, [selectedProject, workspacesInOrg]); + + const activeFilterCount = getActiveFilterCount(getFieldState, watch); + return ( -
-
- {isOrgAuditLogs && workspacesInOrg.length > 0 && ( - ( - - { - if (e === null) { - setValue("secretPath", ""); - } - onChange(e); - }} - placeholder="Select a project..." - options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - - )} - /> - )} - {selectedProject?.type === ProjectType.SecretManager && ( - ( - - onChange(e.target.value)} - /> - - )} - /> - )} - - {selectedProject?.type === ProjectType.SecretManager && ( - ( - - onChange(e.target.value)} - /> - - )} - /> - )} -
-
-
- ( - - - -
- {selectedEventTypes?.length === 1 - ? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0]) - ?.label - : selectedEventTypes?.length === 0 - ? "All events" - : `${selectedEventTypes?.length} events selected`} - -
-
- -
- {eventTypes && eventTypes.length > 0 ? ( - eventTypes.map((eventType) => { - const isSelected = selectedEventTypes?.includes( - eventType.value as EventType - ); - - return ( - eventTypes.length > 1 && event.preventDefault()} - onClick={() => { - if (selectedEventTypes?.includes(eventType.value as EventType)) { - field.onChange( - selectedEventTypes?.filter((e: string) => e !== eventType.value) - ); - } else { - field.onChange([...(selectedEventTypes || []), eventType.value]); - } - }} - key={`event-type-${eventType.value}`} - icon={ - isSelected ? ( - - ) : ( -
- ) - } - iconPos="left" - className="w-[28.4rem] text-sm" - > - {eventType.label} - - ); - }) - ) : ( -
- )} -
- - - - )} - /> - - {!isPending && data && data.length > 0 && !presets?.actorId && ( - ( - - - - )} - /> + + + + > + ( + + onChange(e)} + placeholder="All environments" + options={availableEnvironments.map(({ name, slug }) => ({ + name, + slug + }))} + getOptionValue={(option) => option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + /> + + { + setValue("secretPath", ""); + }} + > + ( + + onChange(e.target.value)} + /> + + )} + /> + + + { + setValue("secretKey", ""); + }} + > + ( + + + setValue("secretKey", e.target.value, { shouldDirty: true }) + } + /> + + )} + /> + + + )} + +
-
-
+ + ); }; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx index 81f1ba169..1878a49e3 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx @@ -6,46 +6,40 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { withPermission } from "@app/hoc"; import { useDebounce } from "@app/hooks"; -import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; +import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; import { usePopUp } from "@app/hooks/usePopUp"; import { LogsFilter } from "./LogsFilter"; import { LogsTable } from "./LogsTable"; -import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types"; +import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types"; type Props = { - presets?: { - actorId?: string; - eventType?: EventType[]; - actorType?: ActorType; - startDate?: Date; - endDate?: Date; - eventMetadata?: Record; - }; - - showFilters?: boolean; - filterClassName?: string; + presets?: Presets; refetchInterval?: number; + showFilters?: boolean; }; export const LogsSection = withPermission( - ({ presets, filterClassName, refetchInterval, showFilters }: Props) => { + ({ presets, refetchInterval, showFilters = true }: Props) => { const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); - const { control, reset, watch, setValue } = useForm({ - resolver: zodResolver(auditLogFilterFormSchema), - defaultValues: { - project: null, - actor: presets?.actorId, - eventType: presets?.eventType || [], - page: 1, - perPage: 10, - startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), // day before today - endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today - } - }); + const { control, reset, watch, getFieldState, resetField, setValue } = + useForm({ + resolver: zodResolver(auditLogFilterFormSchema), + defaultValues: { + project: null, + environment: undefined, + secretKey: "", + secretPath: "", + actor: presets?.actorId, + eventType: presets?.eventType || [], + userAgentType: undefined, + startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), + endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) + } + }); useEffect(() => { if (subscription && !subscription.auditLogs) { @@ -57,6 +51,7 @@ export const LogsSection = withPermission( const userAgentType = watch("userAgentType") as UserAgentType | undefined; const actor = watch("actor"); const projectId = watch("project")?.id; + const environment = watch("environment")?.slug; const secretPath = watch("secretPath"); const secretKey = watch("secretKey"); @@ -67,18 +62,21 @@ export const LogsSection = withPermission( const [debouncedSecretKey] = useDebounce(secretKey!, 500); return ( -
- {showFilters && ( - - )} +
+
+ {showFilters && ( + + )} +
+ diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx index 69b550144..7ac42d40f 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx @@ -1,10 +1,12 @@ import { Fragment } from "react"; import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { Button, EmptyState, + Spinner, Table, TableContainer, TableSkeleton, @@ -52,7 +54,9 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => { -
+ + + Timestamp void; + +export type Presets = { + actorId?: string; + eventType?: EventType[]; + actorType?: ActorType; + startDate?: Date; + endDate?: Date; + eventMetadata?: Record; +}; diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAuditLogsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAuditLogsSection.tsx index 965c0dc67..85454d48e 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAuditLogsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserAuditLogsSection.tsx @@ -1,8 +1,3 @@ -import { useState } from "react"; -import { faFilter } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { IconButton, Tooltip } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { withPermission } from "@app/hoc"; import { OrgUser } from "@app/hooks/api/types"; @@ -14,7 +9,6 @@ type Props = { export const UserAuditLogsSection = withPermission( ({ orgMembership }: Props) => { - const [showFilter, setShowFilter] = useState(false); const { subscription } = useSubscription(); // eslint-disable-next-line no-nested-ternary @@ -23,25 +17,8 @@ export const UserAuditLogsSection = withPermission(

Audit Logs

- - - setShowFilter(!showFilter)} - > -
-

Filter

- -
-
-
{ startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)), eventType: INTEGRATION_EVENTS }} - filterClassName="bg-mineshaft-900 static" />
) : ( diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncAuditLogsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncAuditLogsSection.tsx index cda911e1c..47b359d5e 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncAuditLogsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncAuditLogsSection.tsx @@ -41,7 +41,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => { startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)), eventType: INTEGRATION_EVENTS }} - filterClassName="bg-mineshaft-900 static" /> ) : (
From 042a472f590e045362c7cb2f308479088ca0ce75 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 27 Mar 2025 10:34:52 +0400 Subject: [PATCH 12/40] fix: missing type --- backend/src/ee/services/audit-log/audit-log-types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b9a76dbb5..d918f08e6 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -33,6 +33,7 @@ export type TListProjectAuditLogDTO = { endDate?: string; startDate?: string; projectId?: string; + environment?: string; auditLogActorId?: string; actorType?: ActorType; secretPath?: string; From 2f4c42482d10652d193ac3e7c25db31ec116e4fd Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Thu, 27 Mar 2025 16:45:33 -0300 Subject: [PATCH 13/40] Fix type issue on buildFolderPath --- backend/src/services/secret-folder/secret-folder-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts index 0369c4e11..7e799a826 100644 --- a/backend/src/services/secret-folder/secret-folder-fns.ts +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -5,7 +5,7 @@ export const buildFolderPath = ( foldersMap: Record, depth: number = 0 ): string => { - if (depth > 20) return; + if (depth > 20) return ""; if (!folder.parentId) { return depth === 0 ? "/" : ""; } From 03b0334fa0f895975f6086ebddd9cbff8637b430 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 28 Mar 2025 08:09:55 +0400 Subject: [PATCH 14/40] feat(k8s): pushsecret go templating --- .../kubernetes/infisical-push-secret-crd.mdx | 76 +++++++++++++++++++ k8-operator/api/v1alpha1/common.go | 14 +++- .../api/v1alpha1/infisicalpushsecret_types.go | 15 +++- .../api/v1alpha1/infisicalsecret_types.go | 10 --- .../api/v1alpha1/zz_generated.deepcopy.go | 74 +++++++++++------- ...ts.infisical.com_infisicalpushsecrets.yaml | 13 ++++ .../pushsecret/push-secret-with-template.yaml | 39 ++++++++++ .../{pushSecret.yaml => push-secret.yaml} | 0 .../infisicalpushsecret_helper.go | 69 ++++++++++++++--- .../infisicalsecret/infisicalsecret_helper.go | 19 +---- k8-operator/packages/util/template.go | 20 +++++ 11 files changed, 282 insertions(+), 67 deletions(-) create mode 100644 k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml rename k8-operator/config/samples/crd/pushsecret/{pushSecret.yaml => push-secret.yaml} (100%) create mode 100644 k8-operator/packages/util/template.go diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx index 67fc7f2a8..0d1ee3abf 100644 --- a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -401,6 +401,82 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y +## Using templating to push secrets + +Pushing secrets to Infisical from the operator may not always be enough. +Templating is a useful utility of the Infisical secrets operator that allows you to use Go Templating to template the secrets you want to push to Infisical. +Using Go templates, you can format, combine, and create new key-value pairs of secrets that you want to push to Infisical. + + + + This property controls what secrets are included in your push to Infisica. + When set to `true`, all secrets included in the `push.secret.secretName` Kubernetes secret will be pushed to Infisical. + **Use this option when you would like to push all secrets to Infisical from the secrets operator, but want to template a subset of them.** + + When set to `false`, only secrets defined in the `push.secret.template.data` field of the template will be pushed to Infisical. + Use this option when you would like to push **only** a subset of secrets from the Kubernetes secret to Infisical. + + + Define secret keys and their corresponding templates. + Each data value uses a Golang template with access to all secrets defined in the `push.secret.secretName` Kubernetes secret. + + Secrets are structured as follows: + + ```go + type TemplateSecret struct { + Value string `json:"value"` + SecretPath string `json:"secretPath"` + } + ``` + + #### Example template configuration: + + ```yaml + # This example assumes that the `push-secret-demo` Kubernetes secret contains the following secrets: + # SITE_URL = "https://example.com" + # REGION = "us-east-1" + # OTHER_SECRET = "other-secret" + + push: + secret: + secretName: push-secret-demo + secretNamespace: default + template: + includeAllSecrets: true # Includes all secrets from the `push-secret-demo` Kubernetes secret + data: + SITE_URL: "{{ .SITE_URL.Value }}" + API_URL: "https://api.{{.SITE_URL.Value}}.{{.REGION.Value}}.com" # Will create a new secret in Infisical with the key `API_URL` with the value of the `SITE_URL` and `REGION` secrets + ``` + + To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates. + + ### Available templating functions + + + **Function name**: encodeBase64 + + **Description**: + Given a string, this function will encode the string as a base64 encoded string. + This function is useful when you want to store a string as a base64 encoded value in Infisical. + + **Returns**: The base64 encoded string. + + **Example**: + The example below assumes that the `PLAIN_KEY` secret is stored in your source secret as a plaintext string. + + ```yaml + push: + secret: + secretName: push-secret-demo + secretNamespace: default + template: + includeAllSecrets: true + data: + PLAIN_KEY: "{{ encodeBase64 .PLAIN_KEY.Value }}" # Will be stored in Infisical as a base64 encoded string + ``` + + + ## Applying the InfisicalPushSecret CRD to your cluster Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster. diff --git a/k8-operator/api/v1alpha1/common.go b/k8-operator/api/v1alpha1/common.go index 5631984d1..2489b0b95 100644 --- a/k8-operator/api/v1alpha1/common.go +++ b/k8-operator/api/v1alpha1/common.go @@ -105,7 +105,7 @@ type ManagedKubeSecretConfig struct { // The template to transform the secret data // +kubebuilder:validation:Optional - Template *InfisicalSecretTemplate `json:"template,omitempty"` + Template *SecretTemplate `json:"template,omitempty"` } type ManagedKubeConfigMapConfig struct { @@ -127,5 +127,15 @@ type ManagedKubeConfigMapConfig struct { // The template to transform the secret data // +kubebuilder:validation:Optional - Template *InfisicalSecretTemplate `json:"template,omitempty"` + Template *SecretTemplate `json:"template,omitempty"` +} + +type SecretTemplate struct { + // This injects all retrieved secrets into the top level of your template. + // Secrets defined in the template will take precedence over the injected ones. + // +kubebuilder:validation:Optional + IncludeAllSecrets bool `json:"includeAllSecrets"` + // The template key values + // +kubebuilder:validation:Optional + Data map[string]string `json:"data,omitempty"` } diff --git a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go index 9a1040868..5a7438881 100644 --- a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -16,9 +16,22 @@ type InfisicalPushSecretDestination struct { ProjectID string `json:"projectId"` } +type InfisicalPushSecretSecretSource struct { + // The name of the Kubernetes Secret + // +kubebuilder:validation:Required + SecretName string `json:"secretName"` + + // The name space where the Kubernetes Secret is located + // +kubebuilder:validation:Required + SecretNamespace string `json:"secretNamespace"` + + // +kubebuilder:validation:Optional + Template *SecretTemplate `json:"template,omitempty"` +} + type SecretPush struct { // +kubebuilder:validation:Required - Secret KubeSecretReference `json:"secret"` + Secret InfisicalPushSecretSecretSource `json:"secret"` } // InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index 8f871dfa6..e90c06938 100644 --- a/k8-operator/api/v1alpha1/infisicalsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -116,16 +116,6 @@ type MachineIdentityScopeInWorkspace struct { Recursive bool `json:"recursive"` } -type InfisicalSecretTemplate struct { - // This injects all retrieved secrets into the top level of your template. - // Secrets defined in the template will take precedence over the injected ones. - // +kubebuilder:validation:Optional - IncludeAllSecrets bool `json:"includeAllSecrets"` - // The template key values - // +kubebuilder:validation:Optional - Data map[string]string `json:"data,omitempty"` -} - // InfisicalSecretSpec defines the desired state of InfisicalSecret type InfisicalSecretSpec struct { // +kubebuilder:validation:Optional diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index 11d864ad4..2ad97108d 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -383,7 +383,7 @@ func (in *InfisicalPushSecret) DeepCopyInto(out *InfisicalPushSecret) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -452,12 +452,32 @@ func (in *InfisicalPushSecretList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalPushSecretSecretSource) DeepCopyInto(out *InfisicalPushSecretSecretSource) { + *out = *in + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(SecretTemplate) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretSecretSource. +func (in *InfisicalPushSecretSecretSource) DeepCopy() *InfisicalPushSecretSecretSource { + if in == nil { + return nil + } + out := new(InfisicalPushSecretSecretSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) { *out = *in out.Destination = in.Destination out.Authentication = in.Authentication - out.Push = in.Push + in.Push.DeepCopyInto(&out.Push) out.TLS = in.TLS } @@ -614,28 +634,6 @@ func (in *InfisicalSecretStatus) DeepCopy() *InfisicalSecretStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *InfisicalSecretTemplate) DeepCopyInto(out *InfisicalSecretTemplate) { - *out = *in - if in.Data != nil { - in, out := &in.Data, &out.Data - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalSecretTemplate. -func (in *InfisicalSecretTemplate) DeepCopy() *InfisicalSecretTemplate { - if in == nil { - return nil - } - out := new(InfisicalSecretTemplate) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubeSecretReference) DeepCopyInto(out *KubeSecretReference) { *out = *in @@ -703,7 +701,7 @@ func (in *ManagedKubeConfigMapConfig) DeepCopyInto(out *ManagedKubeConfigMapConf *out = *in if in.Template != nil { in, out := &in.Template, &out.Template - *out = new(InfisicalSecretTemplate) + *out = new(SecretTemplate) (*in).DeepCopyInto(*out) } } @@ -723,7 +721,7 @@ func (in *ManagedKubeSecretConfig) DeepCopyInto(out *ManagedKubeSecretConfig) { *out = *in if in.Template != nil { in, out := &in.Template, &out.Template - *out = new(InfisicalSecretTemplate) + *out = new(SecretTemplate) (*in).DeepCopyInto(*out) } } @@ -741,7 +739,7 @@ func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretPush) DeepCopyInto(out *SecretPush) { *out = *in - out.Secret = in.Secret + in.Secret.DeepCopyInto(&out.Secret) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush. @@ -769,6 +767,28 @@ func (in *SecretScopeInWorkspace) DeepCopy() *SecretScopeInWorkspace { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretTemplate) DeepCopyInto(out *SecretTemplate) { + *out = *in + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretTemplate. +func (in *SecretTemplate) DeepCopy() *SecretTemplate { + if in == nil { + return nil + } + out := new(SecretTemplate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) { *out = *in diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml index a12ec9dbe..25fafd98a 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -137,6 +137,19 @@ spec: description: The name space where the Kubernetes Secret is located type: string + template: + properties: + data: + additionalProperties: + type: string + description: The template key values + type: object + includeAllSecrets: + description: This injects all retrieved secrets into the + top level of your template. Secrets defined in the template + will take precedence over the injected ones. + type: boolean + type: object required: - secretName - secretNamespace diff --git a/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml new file mode 100644 index 000000000..97fa9ace8 --- /dev/null +++ b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml @@ -0,0 +1,39 @@ +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalPushSecret +metadata: + name: infisical-api-secret-sample-push +spec: + resyncInterval: 1m + hostAPI: http://localhost:8080/api + + # Optional, defaults to replacement. + updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. + + # Optional, defaults to no deletion. + deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted. + + destination: + projectId: + environmentSlug: + secretsPath: + + push: + secret: + secretName: push-secret-demo # Secret CRD + secretNamespace: default + template: + includeAllSecrets: false + data: + # Encodes the data so it's stored as base64 in Infisical. + API_KEY: "{{ .API_KEY.Value }}" + DATABASE_URL: "{{ .DATABASE_URL.Value }}" + ENCRYPTION_KEY: "{{ .ENCRYPTION_KEY.Value }}" + OTHER_VALUE: "{{ encodeBase64 .API_KEY.Value }} {{ encodeBase64 .DATABASE_URL.Value }} {{ encodeBase64 .ENCRYPTION_KEY.Value }}" + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + universalAuth: + credentialsRef: + secretName: universal-auth-credentials + secretNamespace: default diff --git a/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml b/k8-operator/config/samples/crd/pushsecret/push-secret.yaml similarity index 100% rename from k8-operator/config/samples/crd/pushsecret/pushSecret.yaml rename to k8-operator/config/samples/crd/pushsecret/push-secret.yaml diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index 47fc8e69b..1bcecfba0 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -1,16 +1,20 @@ package controllers import ( + "bytes" "context" "errors" "fmt" "strings" + "text/template" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/constants" + "github.com/Infisical/infisical/k8-operator/packages/model" "github.com/Infisical/infisical/k8-operator/packages/util" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -101,6 +105,48 @@ func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSec infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables } +func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) { + + processedSecrets := make(map[string]string) + + sourceSecrets := make(map[string]model.SecretTemplateOptions) + for key, value := range kubePushSecret.Data { + + sourceSecrets[key] = model.SecretTemplateOptions{ + Value: string(value), + SecretPath: destination.SecretsPath, + } + } + + if infisicalPushSecret.Spec.Push.Secret.Template == nil || (infisicalPushSecret.Spec.Push.Secret.Template != nil && infisicalPushSecret.Spec.Push.Secret.Template.IncludeAllSecrets) { + for key, value := range kubePushSecret.Data { + processedSecrets[key] = string(value) + } + } + + if infisicalPushSecret.Spec.Push.Secret.Template != nil && + len(infisicalPushSecret.Spec.Push.Secret.Template.Data) > 0 { + + for templateKey, userTemplate := range infisicalPushSecret.Spec.Push.Secret.Template.Data { + + tmpl, err := template.New("push-secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) + if err != nil { + return nil, fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) + } + + buf := bytes.NewBuffer(nil) + err = tmpl.Execute(buf, sourceSecrets) + if err != nil { + return nil, fmt.Errorf("unable to execute template: %s [err=%v]", templateKey, err) + } + + processedSecrets[templateKey] = buf.String() + } + } + + return processedSecrets, nil +} + func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret v1alpha1.InfisicalPushSecret) error { resourceVariables := r.getResourceVariables(infisicalPushSecret) @@ -134,10 +180,9 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context return fmt.Errorf("unable to fetch kube secret [err=%s]", err) } - var kubeSecrets = make(map[string]string) - - for key, value := range kubePushSecret.Data { - kubeSecrets[key] = string(value) + processedSecrets, err := r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination) + if err != nil { + return fmt.Errorf("unable to process templated secrets [err=%s]", err) } destination := infisicalPushSecret.Spec.Destination @@ -191,7 +236,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] ) - for secretKey, secretValue := range kubeSecrets { + for secretKey, secretValue := range processedSecrets { if exists := getExistingSecretByKey(secretKey); exists != nil { if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { @@ -280,7 +325,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context // We need to check if any of the secrets have been removed in the new kube secret for _, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets { - if _, ok := kubeSecrets[managedSecretKey]; !ok { + if _, ok := processedSecrets[managedSecretKey]; !ok { // Secret has been removed, verify that the secret is managed by the operator if getExistingSecretByKey(managedSecretKey) != nil { @@ -305,7 +350,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context } // We need to check if any new secrets have been added in the kube secret - for currentSecretKey := range kubeSecrets { + for currentSecretKey := range processedSecrets { if exists := getExistingSecretByKey(currentSecretKey); exists == nil { @@ -317,7 +362,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{ SecretKey: currentSecretKey, - SecretValue: kubeSecrets[currentSecretKey], + SecretValue: processedSecrets[currentSecretKey], ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, @@ -336,12 +381,12 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context existingSecret := getExistingSecretByKey(currentSecretKey) - if existingSecret != nil && existingSecret.SecretValue != kubeSecrets[currentSecretKey] { + if existingSecret != nil && existingSecret.SecretValue != processedSecrets[currentSecretKey] { logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", currentSecretKey)) updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ SecretKey: currentSecretKey, - NewSecretValue: kubeSecrets[currentSecretKey], + NewSecretValue: processedSecrets[currentSecretKey], ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, @@ -353,7 +398,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context continue } - updateExistingSecretByKey(currentSecretKey, kubeSecrets[currentSecretKey]) + updateExistingSecretByKey(currentSecretKey, processedSecrets[currentSecretKey]) infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey } } @@ -361,7 +406,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context } // Check if any of the existing secrets values have changed - for secretKey, secretValue := range kubeSecrets { + for secretKey, secretValue := range processedSecrets { existingSecret := getExistingSecretByKey(secretKey) diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go index eb41fe6e9..54afbfa90 100644 --- a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go @@ -3,7 +3,6 @@ package controllers import ( "bytes" "context" - "encoding/base64" "errors" "fmt" "strings" @@ -156,16 +155,6 @@ func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKub return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil } -var infisicalSecretTemplateFunctions = template.FuncMap{ - "decodeBase64ToBytes": func(encodedString string) string { - decoded, err := base64.StdEncoding.DecodeString(encodedString) - if err != nil { - panic(fmt.Sprintf("Error: %v", err)) - } - return string(decoded) - }, -} - func convertBinaryToStringMap(binaryMap map[string][]byte) map[string]string { stringMap := make(map[string]string) for k, v := range binaryMap { @@ -177,7 +166,7 @@ func convertBinaryToStringMap(binaryMap map[string][]byte) map[string]string { func (r *InfisicalSecretReconciler) createInfisicalManagedKubeResource(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedSecretReferenceInterface interface{}, secretsFromAPI []model.SingleEnvironmentVariable, ETag string, resourceType constants.ManagedKubeResourceType) error { plainProcessedSecrets := make(map[string][]byte) - var managedTemplateData *v1alpha1.InfisicalSecretTemplate + var managedTemplateData *v1alpha1.SecretTemplate if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET { managedTemplateData = managedSecretReferenceInterface.(v1alpha1.ManagedKubeSecretConfig).Template @@ -201,7 +190,7 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeResource(ctx conte } for templateKey, userTemplate := range managedTemplateData.Data { - tmpl, err := template.New("secret-templates").Funcs(infisicalSecretTemplateFunctions).Parse(userTemplate) + tmpl, err := template.New("secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) if err != nil { return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } @@ -322,7 +311,7 @@ func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context } for templateKey, userTemplate := range managedTemplateData.Data { - tmpl, err := template.New("secret-templates").Funcs(infisicalSecretTemplateFunctions).Parse(userTemplate) + tmpl, err := template.New("secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) if err != nil { return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } @@ -373,7 +362,7 @@ func (r *InfisicalSecretReconciler) updateInfisicalManagedConfigMap(ctx context. } for templateKey, userTemplate := range managedTemplateData.Data { - tmpl, err := template.New("secret-templates").Funcs(infisicalSecretTemplateFunctions).Parse(userTemplate) + tmpl, err := template.New("secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) if err != nil { return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } diff --git a/k8-operator/packages/util/template.go b/k8-operator/packages/util/template.go new file mode 100644 index 000000000..c8ed98e48 --- /dev/null +++ b/k8-operator/packages/util/template.go @@ -0,0 +1,20 @@ +package util + +import ( + "encoding/base64" + "fmt" + "text/template" +) + +var InfisicalSecretTemplateFunctions = template.FuncMap{ + "decodeBase64ToBytes": func(encodedString string) string { + decoded, err := base64.StdEncoding.DecodeString(encodedString) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + return string(decoded) + }, + "encodeBase64": func(plainString string) string { + return base64.StdEncoding.EncodeToString([]byte(plainString)) + }, +} From 10a3c7015eabf1f87209728dac63139801449f24 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 28 Mar 2025 08:12:11 +0400 Subject: [PATCH 15/40] Update push-secret-with-template.yaml --- .../pushsecret/push-secret-with-template.yaml | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml index 97fa9ace8..11734dd18 100644 --- a/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml +++ b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml @@ -4,7 +4,7 @@ metadata: name: infisical-api-secret-sample-push spec: resyncInterval: 1m - hostAPI: http://localhost:8080/api + hostAPI: https://app.infisical.com/api # Optional, defaults to replacement. updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. @@ -33,7 +33,21 @@ spec: # Only have one authentication method defined or you are likely to run into authentication issues. # Remove all except one authentication method. authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: universalAuth: credentialsRef: - secretName: universal-auth-credentials - secretNamespace: default + secretName: # universal-auth-credentials + secretNamespace: # default From b0cacc5a4a9dbb3fa7f2255721ca42d57421bb35 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 28 Mar 2025 11:09:56 -0300 Subject: [PATCH 16/40] General improvements to Access Tree view --- .../secret-folder/secret-folder-fns.ts | 5 +- .../secret-folder/secret-folder-service.ts | 27 +++- .../permissions/AccessTree/AccessTree.tsx | 15 +-- .../permissions/AccessTree/hooks/index.ts | 43 +++---- .../permissions/AccessTree/nodes/RoleNode.tsx | 95 ++++++++++++-- .../AccessTree/nodes/ShowMoreButtonNode.tsx | 34 ++--- .../AccessTree/utils/createRoleNode.ts | 18 ++- .../AccessTree/utils/createShowMoreNode.ts | 26 +++- .../AccessTree/utils/positionElements.ts | 116 +++++++++--------- 9 files changed, 256 insertions(+), 123 deletions(-) diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts index 7e799a826..a3783a1b9 100644 --- a/backend/src/services/secret-folder/secret-folder-fns.ts +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -1,11 +1,14 @@ import { TSecretFolders } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; export const buildFolderPath = ( folder: TSecretFolders, foldersMap: Record, depth: number = 0 ): string => { - if (depth > 20) return ""; + if (depth > 20) { + throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" }); + } if (!folder.parentId) { return depth === 0 ? "/" : ""; } diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index d4f58a1d4..96bcdd6f3 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -619,10 +619,29 @@ export const secretFolderServiceFactory = ({ const relevantFolders = folders.filter((folder) => folder.envId === env.id); const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder])); - const foldersWithPath = relevantFolders.map((folder) => ({ - ...folder, - path: buildFolderPath(folder, foldersMap) - })); + const foldersWithPath = relevantFolders + .map((folder) => { + try { + return { + ...folder, + path: buildFolderPath(folder, foldersMap) + }; + } catch (error) { + return null; + } + }) + .filter(Boolean) as { + path: string; + id: string; + createdAt: Date; + updatedAt: Date; + name: string; + envId: string; + version?: number | null | undefined; + parentId?: string | null | undefined; + isReserved?: boolean | undefined; + description?: string | undefined; + }[]; return [env.slug, { ...env, folders: foldersWithPath }]; }) diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx index 265e87623..4ba2c718b 100644 --- a/frontend/src/components/permissions/AccessTree/AccessTree.tsx +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from "react"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { faAnglesUp, - faArrowsUpDownLeftRight, faArrowUpRightFromSquare, faDownLeftAndUpRightToCenter, faUpRightAndDownLeftFromCenter, @@ -29,7 +28,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput"; import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode"; -import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components"; +import { AccessTreeErrorBoundary, AccessTreeProvider } from "./components"; import { BasePermissionEdge } from "./edges"; import { useAccessTree } from "./hooks"; import { FolderNode, RoleNode } from "./nodes"; @@ -231,14 +230,12 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { /> )} - - - fitView({ duration: 800 })}> - - - - + fitView({ duration: 800 })} + > diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index 8651ea543..a03f8fa0f 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -89,17 +89,18 @@ export const useAccessTree = ( const { folders } = environmentsFolders[environment]; setTotalFolderCount(folders.length); - - const searchPathFolder = folders.find((folder) => folder.path === searchPath); + const groupedFolders: Record = {}; const filteredFolders = folders.filter((folder) => { - if (folder.path === searchPath) { + if (folder.path.startsWith(searchPath)) { return true; } if ( - folder.path.startsWith(searchPath) && - (searchPath === "/" || folder.path.charAt(searchPath.length) === "/") + searchPath.startsWith(folder.path) && + (folder.path === "/" || + searchPath === folder.path || + searchPath.indexOf("/", folder.path.length) === folder.path.length) ) { return true; } @@ -107,17 +108,11 @@ export const useAccessTree = ( return false; }); - const rootFolder = searchPathFolder || filteredFolders.find((f) => f.path === "/"); - - const groupedFolders: Record = {}; - filteredFolders.forEach((folder) => { const parentId = folder.parentId || ""; - if (!groupedFolders[parentId]) { groupedFolders[parentId] = []; } - groupedFolders[parentId].push(folder); }); @@ -132,18 +127,7 @@ export const useAccessTree = ( }; }); - if (rootFolder) { - setLevelFolderMap({ - ...newLevelFolderMap, - __rootFolderId: { - folders: [rootFolder], - visibleCount: 1, - hasMore: false - } - }); - } else { - setLevelFolderMap(newLevelFolderMap); - } + setLevelFolderMap(newLevelFolderMap); }, [permissions, environmentsFolders, environment, subject, secretName, searchPath]); useEffect(() => { @@ -155,11 +139,14 @@ export const useAccessTree = ( ) return; - const { name } = environmentsFolders[environment]; + const { slug } = environmentsFolders[environment]; const roleNode = createRoleNode({ subject, - environment: name + environment: slug, + environments: environmentsFolders, + onSubjectChange: setSubject, + onEnvironmentChange: setEnvironment }); const actionRuleMap = getSubjectActionRuleMap(subject, permissions); @@ -252,7 +239,8 @@ export const useAccessTree = ( const showMoreButtonNode = createShowMoreNode({ parentId: key, onClick: () => showMoreFolders(key), - remaining: levelData.folders.length - levelData.visibleCount + remaining: levelData.folders.length - levelData.visibleCount, + subject }); addMoreButtons.push(showMoreButtonNode); @@ -261,8 +249,7 @@ export const useAccessTree = ( createBaseEdge({ source: key, target: showMoreButtonNode.id, - access: PermissionAccess.Full, - hideEdge: true + access: PermissionAccess.Partial }) ); } diff --git a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx index d38d896f2..f6ffe212e 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx @@ -1,10 +1,42 @@ +import { Dispatch, SetStateAction } from "react"; +import { faFileImport, faFolder, faKey, faLock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Handle, NodeProps, Position } from "@xyflow/react"; +import { Select, SelectItem } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; +import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; + import { createRoleNode } from "../utils"; +const getSubjectIcon = (subject: ProjectPermissionSub) => { + switch (subject) { + case ProjectPermissionSub.Secrets: + return ; + case ProjectPermissionSub.SecretFolders: + return ; + case ProjectPermissionSub.DynamicSecrets: + return ; + case ProjectPermissionSub.SecretImports: + return ; + default: + return ; + } +}; + +const formatLabel = (text: string) => { + return text.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +}; + export const RoleNode = ({ - data: { subject, environment } -}: NodeProps & { data: ReturnType["data"] }) => { + data: { subject, environment, onSubjectChange, onEnvironmentChange, environments } +}: NodeProps & { + data: ReturnType["data"] & { + onSubjectChange: Dispatch>; + onEnvironmentChange: (value: string) => void; + environments: TProjectEnvironmentsFolders; + }; +}) => { return ( <> -
-
- {subject.replace("-", " ")} Access -
-

{environment}

+
+
+
+
Subject
+ +
+ +
+
Environment
+
diff --git a/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx index 2a6cabf43..dc412d8ca 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx @@ -12,20 +12,26 @@ export const ShowMoreButtonNode = ({ const tooltipText = `${remaining} ${remaining === 1 ? "folder is" : "folders are"} hidden. Click to show ${remaining > 10 ? "10 more" : ""}`; return ( - <> - +
+ - - - - +
+ + + +
+
); }; diff --git a/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts b/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts index 354a69482..69ec60896 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts @@ -1,17 +1,31 @@ +import { Dispatch, SetStateAction } from "react"; + +import { ProjectPermissionSub } from "@app/context"; +import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; + import { PermissionNode } from "../types"; export const createRoleNode = ({ subject, - environment + environment, + environments, + onSubjectChange, + onEnvironmentChange }: { subject: string; environment: string; + environments: TProjectEnvironmentsFolders; + onSubjectChange: Dispatch>; + onEnvironmentChange: (value: string) => void; }) => ({ id: `role-${subject}-${environment}`, position: { x: 0, y: 0 }, data: { subject, - environment + environment, + environments, + onSubjectChange, + onEnvironmentChange }, type: PermissionNode.Role, height: 48, diff --git a/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts b/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts index e5758ffc9..f12cd598e 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts @@ -1,14 +1,30 @@ +import { ProjectPermissionSub } from "@app/context"; + import { PermissionNode } from "../types"; export const createShowMoreNode = ({ parentId, onClick, - remaining + remaining, + subject }: { parentId: string | null; onClick: () => void; remaining: number; + subject: ProjectPermissionSub; }) => { + let height: number; + + switch (subject) { + case ProjectPermissionSub.DynamicSecrets: + height = 130; + break; + case ProjectPermissionSub.Secrets: + height = 85; + break; + default: + height = 64; + } const id = `show-more-${parentId || "root"}`; return { id, @@ -19,7 +35,11 @@ export const createShowMoreNode = ({ onClick, remaining }, - width: 100, - height: 40 + width: 150, + height, + style: { + background: "transparent", + border: "none" + } }; }; diff --git a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts index 8b90a752f..c4daf6317 100644 --- a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts +++ b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts @@ -2,8 +2,10 @@ import Dagre from "@dagrejs/dagre"; import { Edge, Node } from "@xyflow/react"; export const positionElements = (nodes: Node[], edges: Edge[]) => { - const regularNodes = nodes.filter((node) => node.type !== "showMoreButton"); const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton"); + const showMoreParentIds = new Set( + showMoreNodes.map((node) => node.data.parentId).filter(Boolean) + ); const nodeMap: Record = {}; const childrenMap: Record = {}; @@ -17,75 +19,79 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => { const dagre = new Dagre.graphlib.Graph({ directed: true }) .setDefaultEdgeLabel(() => ({})) - .setGraph({ rankdir: "TB" }); + .setGraph({ + rankdir: "TB", + nodesep: 50, + ranksep: 70 + }); + + nodes.forEach((node) => { + dagre.setNode(node.id, { + width: node.width || 150, + height: node.height || 40 + }); + }); edges.forEach((edge) => dagre.setEdge(edge.source, edge.target)); - regularNodes.forEach((node) => dagre.setNode(node.id, node)); - Dagre.layout(dagre, {}); - const positionedNodes = regularNodes.map((node) => { + const positionedNodes = nodes.map((node) => { const { x, y } = dagre.node(node.id); - const positionedNode = { - ...node, - position: { - x: x - (node.width ? node.width / 2 : 0), - y: y - (node.height ? node.height / 2 : 0) - } - }; - - nodeMap[node.id] = positionedNode; - - return positionedNode; - }); - - const findLastChildNode = (parentId: string): Node | undefined => { - const childrenIds = childrenMap[parentId] || []; - if (childrenIds.length === 0) return undefined; - - const childNodes = childrenIds.map((id) => nodeMap[id]).filter(Boolean); - - if (childNodes.length === 0) return undefined; - - childNodes.sort((a, b) => { - if (a.position.y === b.position.y) { - return b.position.x - a.position.x; - } - return b.position.y - a.position.y; - }); - - return childNodes[0]; - }; - - const positionedShowMoreNodes = showMoreNodes.map((node) => { - const parentId = node.data.parentId as string; - - const parentNode = nodeMap[parentId] || positionedNodes[0]; - const lastChildNode = findLastChildNode(parentId); - - const referenceNode = lastChildNode || parentNode; - - const referenceX = referenceNode.position.x; - const referenceY = referenceNode.position.y; - - const referenceWidth = referenceNode.width || 150; - - const buttonX = referenceX + referenceWidth - 85; - const buttonY = referenceY - 25; + if (node.type === "role") { + return { + ...node, + position: { + x: x - (node.width ? node.width / 2 : 0), + y: y - 150 + } + }; + } return { ...node, position: { - x: buttonX, - y: buttonY - } + x: x - (node.width ? node.width / 2 : 0), + y: y - (node.height ? node.height / 2 : 0) + }, + style: node.type === "showMoreButton" ? { ...node.style, zIndex: 10 } : node.style }; }); + positionedNodes.forEach((node) => { + nodeMap[node.id] = node; + }); + + Array.from(showMoreParentIds).forEach((parentId) => { + const showMoreNodeIndex = positionedNodes.findIndex( + (node) => node.type === "showMoreButton" && node.data.parentId === parentId + ); + + if (showMoreNodeIndex !== -1) { + const siblings = positionedNodes.filter( + (node) => node.data?.parentId === parentId && node.type !== "showMoreButton" + ); + + if (siblings.length > 0) { + const rightmostSibling = siblings.reduce( + (rightmost, current) => (current.position.x > rightmost.position.x ? current : rightmost), + siblings[0] + ); + + positionedNodes[showMoreNodeIndex] = { + ...positionedNodes[showMoreNodeIndex], + position: { + x: rightmostSibling.position.x + (rightmostSibling.width || 150) + 30, + y: rightmostSibling.position.y + } + }; + } + } + }); + return { - nodes: [...positionedNodes, ...positionedShowMoreNodes], + nodes: positionedNodes, edges }; }; From d7f9cff43e23a7b62562cb778f28ec39df910d93 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 29 Mar 2025 03:27:35 +0400 Subject: [PATCH 17/40] feat(k8s): large expansion of templating functions --- .infisicalignore | 2 + .../pushsecret/push-secret-with-template.yaml | 54 ++++++- .../samples/crd/pushsecret/push-secret.yaml | 2 +- .../source-secret-with-templating.yaml | 91 +++++++++++ .../samples/crd/pushsecret/source-secret.yaml | 9 ++ .../samples/crd/pushsecret/sourceSecret.yaml | 9 -- .../infisicalpushsecret_helper.go | 5 +- .../infisicalsecret/infisicalsecret_helper.go | 9 +- k8-operator/go.mod | 31 +++- k8-operator/go.sum | 68 +++++++-- k8-operator/packages/template/base64.go | 18 +++ k8-operator/packages/template/json.go | 32 ++++ k8-operator/packages/template/jwk.go | 43 ++++++ k8-operator/packages/template/pem.go | 98 ++++++++++++ k8-operator/packages/template/pem_chain.go | 117 ++++++++++++++ k8-operator/packages/template/pkcs12.go | 144 ++++++++++++++++++ k8-operator/packages/template/template.go | 77 ++++++++++ k8-operator/packages/template/yaml.go | 30 ++++ k8-operator/packages/util/template.go | 20 --- 19 files changed, 797 insertions(+), 62 deletions(-) create mode 100644 k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml create mode 100644 k8-operator/config/samples/crd/pushsecret/source-secret.yaml delete mode 100644 k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml create mode 100644 k8-operator/packages/template/base64.go create mode 100644 k8-operator/packages/template/json.go create mode 100644 k8-operator/packages/template/jwk.go create mode 100644 k8-operator/packages/template/pem.go create mode 100644 k8-operator/packages/template/pem_chain.go create mode 100644 k8-operator/packages/template/pkcs12.go create mode 100644 k8-operator/packages/template/template.go create mode 100644 k8-operator/packages/template/yaml.go delete mode 100644 k8-operator/packages/util/template.go diff --git a/.infisicalignore b/.infisicalignore index 8072078d4..b80ecaad5 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -12,3 +12,5 @@ docs/cli/commands/bootstrap.mdx:jwt:86 docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:102 docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72 +k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11 +k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52 diff --git a/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml index 11734dd18..805df3a52 100644 --- a/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml +++ b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml @@ -4,7 +4,7 @@ metadata: name: infisical-api-secret-sample-push spec: resyncInterval: 1m - hostAPI: https://app.infisical.com/api + hostAPI: https://app.infisical.com/api # This is the default hostAPI for the Infisical API # Optional, defaults to replacement. updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. @@ -14,21 +14,58 @@ spec: destination: projectId: - environmentSlug: + environmentSlug: push: secret: - secretName: push-secret-demo # Secret CRD + secretName: push-secret-demo-with-templating secretNamespace: default template: includeAllSecrets: false data: - # Encodes the data so it's stored as base64 in Infisical. - API_KEY: "{{ .API_KEY.Value }}" - DATABASE_URL: "{{ .DATABASE_URL.Value }}" - ENCRYPTION_KEY: "{{ .ENCRYPTION_KEY.Value }}" - OTHER_VALUE: "{{ encodeBase64 .API_KEY.Value }} {{ encodeBase64 .DATABASE_URL.Value }} {{ encodeBase64 .ENCRYPTION_KEY.Value }}" + PKCS12_CERT_NO_PASSWORD: "{{ .PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert }}" + PKCS12_KEY_NO_PASSWORD: "{{ .PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12key }}" + + PKCS12_CERT_WITH_PASSWORD: '{{ .PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12certPass "123456" }}' + PKCS12_KEY_WITH_PASSWORD: '{{ .PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12keyPass "123456" }}' + + PEM_TO_PKCS12_PASS: '{{ pemToPkcs12Pass + (.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12certPass "123456") + (.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12keyPass "123456") + "123456" }}' + PEM_TO_PKCS12_NO_PASSWORD: "{{ pemToPkcs12 + (.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert) + (.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12key) + }}" + + FULL_PEM_TO_PKCS12_PASS: '{{ fullPemToPkcs12Pass + (.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12certPass "123456") + (.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12keyPass "123456") + "123456" }}' + FULL_PEM_TO_PKCS12_NO_PASSWORD: "{{ fullPemToPkcs12 + (.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert) + (.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12key) + }}" + + FILTERED_PEM_CERT: '{{ filterPEM "CERTIFICATE" (printf "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" .JWK_PRIVATE_RSA_PKCS8.Value .PKCS12_CONTENT_NO_PASSWORD.Value) }}' + FILTERED_PEM_KEY: '{{ filterPEM "PRIVATE KEY" (printf "-----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----\n-----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----" .JWK_PRIVATE_RSA_PKCS8.Value .PKCS12_CONTENT_NO_PASSWORD.Value) }}' + + # Will be empty with our current test data as there is no chain + CERT_CHAIN: '{{ filterCertChain "CERTIFICATE" (.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert) }}' + + JWK_RSA_PUBLIC_PEM: "{{ jwkPublicKeyPem .JWK_PUB_RSA.Value }}" + JWK_ECDSA_PUBLIC_PEM: "{{ jwkPublicKeyPem .JWK_PUB_ECDSA.Value }}" + + JWK_ECDSA_PRIVATE_PEM: "{{ jwkPrivateKeyPem .JWK_PRIV_ECDSA.Value }}" + JWK_RSA_PRIVATE_PEM: "{{ jwkPrivateKeyPem .JWK_PRIV_RSA.Value }}" + + JSON_STR_TO_YAML: "{{ .TEST_JSON_DATA.Value | fromJsonStringToJson | toYaml }}" + + FROM_YAML_TO_JSON: "{{ .TEST_YAML_STRING.Value | fromYaml | toJson }}" + YAML_ROUNDTRIP: "{{ .TEST_YAML_STRING.Value | fromYaml | toYaml }}" + + TEST_LOWERCASE_STRING: "{{ .TEST_LOWERCASE_STRING.Value | upper }}" # Only have one authentication method defined or you are likely to run into authentication issues. # Remove all except one authentication method. @@ -51,3 +88,4 @@ spec: credentialsRef: secretName: # universal-auth-credentials secretNamespace: # default + diff --git a/k8-operator/config/samples/crd/pushsecret/push-secret.yaml b/k8-operator/config/samples/crd/pushsecret/push-secret.yaml index f1738fc47..b6a132987 100644 --- a/k8-operator/config/samples/crd/pushsecret/push-secret.yaml +++ b/k8-operator/config/samples/crd/pushsecret/push-secret.yaml @@ -19,7 +19,7 @@ spec: push: secret: - secretName: push-secret-demo # Secret CRD + secretName: push-secret-source-secret # Secret CRD secretNamespace: default # Only have one authentication method defined or you are likely to run into authentication issues. diff --git a/k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml b/k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml new file mode 100644 index 000000000..79a12a4d4 --- /dev/null +++ b/k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml @@ -0,0 +1,91 @@ +# This is the source secret that you can use to demo the advanced templating functionality seen in `push-secret-with-template.yaml` + +apiVersion: v1 +kind: Secret +metadata: + name: push-secret-demo-with-templating + namespace: default +stringData: + PKCS12_CONTENT_NO_PASSWORD: MIIJYQIBAzCCCScGCSqGSIb3DQEHAaCCCRgEggkUMIIJEDCCA8cGCSqGSIb3DQEHBqCCA7gwggO0AgEAMIIDrQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQInZmyWpNTPS4CAggAgIIDgPzZTmogBRiLP0NJZEUghZ3Oh1aqHJJ32HKgXUpD5BJ/5AvpUL9FC7m6a3GD++P1On/35J9N50bDjfBJjJrl2zpA143bzltPQBOK30cBJjNsCeN2Dq1dcsvJZfEy20z75NduXjMF6/qs4BbE+1E6nYFYVNHUybFnaQwSx7+2/2OMbXbcFpt4bv3HTw0YLw2pZeW/4/4A9d+tC9UdVQTTyNbI8l9nf1aeaaPsw1keVLmHurmTihfwh469FvjgwiHUP/P3ZCn1tOpWDR8ck0j+ru6imVP2hn+Kvk6svllmYqo3A5DnDRoF/Cl9R0DAPyS0lw7BeGskgTm7B79mzVitTbzRnIUP+sGJjc1AVghnitfcX4ffv8gq5xWaKGucO/IZXbPBoe7tMhKZmsirKzD4RBhC3nMyrwaHJB6PqUwxMQGMLbuHe7GlWhJAyFlcOTt5dgNl+axIkWdisoKNinYYeOuxudqyX6yPfsyaRCV5MEez3Wu+59MENGlGDRWbw61QuwsZkr1bAT2SJrQ/zHn5aGAluQZ1csJhKQ34iy1Ml9K9F4Zh3/2OWPs0u6+JCb1PC1vChBkguqcqQtEcikRwR9dNF9cdMB1T1Xk5GqlmOPaigkYzGWLgtl8cV5/Zl0m2j77mX9x4HVCTercAABGf9JcCLzSCo04c5OwIYtWUXBkux5n2VI2ZIuS1KF+r6JNyL3lg/D8LColzDUP/6tQCBVVgMar3iLblM17wPMTDMR5Bn+NvenwJj6FWaGGMtdjygtN+oSHpNDbVygfGQy+jEgUtK7yw0uh/WKBMWVw1E6iNuhb8HIyCFtQon8sDkuZ81czOpR3Ta1SWUWrZD+pjpL2Z4y8Nc2wt9pVPvLFOTn+GDFVqGpde3kovh3GfJjYCG/HI5rXZyziflDOoSy0SyG6aVCG4ZqW2LTymoVN/kxf+skqAweX1vxvvJniiv8HgYfEASFUWear4uT641d1YwcEIawNv4n+GKBilK/7ODl2QL86svwqIcbyiJrneyU2tHymKzGcU2VxmSgf8EnjqGuIEo7WXOpk0oUMcvYrM73cgzZ3BchUDIN0KWSDI+vDcVY82dbI39KM6dtOJFAx3kEdms/gdSqZtmHUIeArGp+8caCCAK/W+4wTOvtisK+6MtzdMz6P93N78N4Vo6cs3dkj6t/6tgNog5SCfwlOEyUpmMIIFQQYJKoZIhvcNAQcBoIIFMgSCBS4wggUqMIIFJgYLKoZIhvcNAQwKAQKgggTuMIIE6jAcBgoqhkiG9w0BDAEDMA4ECHVnarQ94cqlAgIIAASCBMgUvEVKsUcqEvYJEJ9JixgB0W3uhSi/Espt931a/mwx5Ja2K7vjlttaOct3Zc8umVrP5C322tmHz9QDVPj3Bln8CGfofC/8Nb6+SDeofmYaQYReOZpZGksEBs4P3yURl8wQpIkG31Oyf3urDTJdplfDrzu6XpEpIf7RicIR+Zh4Q1+F75XwPo52/yNs8q/kVV8H97gSRqQ2GixIdyNu+JLtNjdwAERHy4DeQjwgiMCdL+xMfN+WJyIvkLZDoy9bacXeG4IcQM+n84272C6j1a0BPaOm0K5A7I0H1zpXOJiWfn3MrT4LHDudrQoIWUOvcJjWaIM/KyghotDN50THKN9qCEE9SmtfWXGGFaJmyxbUDFizBIAsFshNtMs/47PoInTSNwzxNvUUQ3ap93iquGZ9EaZAMY2HQHW/QJIQ70IbtcHU28Bus/hrMcV0X9D1p4UeHuk37W7aCrL6hS+ac9pmzwmcDBwZUliyInxRmqCCerjg2ojAM9SVg8FrpQUErP+BOaoCBwQqLLiz9BM+3tUQc/8MyaBHq+c2dUoPfvipDIQXYiq66CkjmPHxPFEL1l9d9oBFoIGkt6SIHDjWnTPc5q5SvJ9tz8Dp1k/1HQSA8OUS6j+XySYuGe8xTvN/oUpVRswef2Qd/kxZlc1FJ4lVAXvbW7C7772l14BJv/WULcFH4Sn83rlL3YwHr4vJMf6wLahn7oQPI0VFSQiiOOb/+gkiTrwO3Gz+HXOkUwaKnW85PeoIt3/q1u0CRl64mUjqCegi7RMY9Q9tRMlD5yx0RsH7mc4b6Eg/3IwGu8VQmZCO5W2unCpfzzyrOx7OaGGaW4RJ2Mx7bJ8uV9HU8MbbNntmc9oxebPdDnBmbt8p8t4ZZxC+zcqcXi3TxACXmwnasogQEi0d0ttXkB5cnDCG00Y8WPdNIWfJdIQh8Hj16LAMYWUacz/J0kLP99ENQntZibVw/Q3zZtHSF5tmsYp7o1HglBpRwLTcd026YTrxB+VCEiUYy4hH6a38oEEpY7wTIiRmEBQPIRM0HUOqVh4z6TNzRx6iIhrQEvg06B8U6iVPqy8FGDkhf3P55Ed95/Rw6uSdlMTHng+Q4aG00k4qKdKOyv55IXPcvEzAeVNBuesknaS8x7Eb/I5mHSoZU3RYAEFGbehUkvkhNr3Xq7/W/400AKiliravJq8j/qKIZ9hAVUWOps09F/4peYfLXM1AhxWWGa5QqvwFkClM+uRyqIRGJwl2Z7asl4sWVXbwtb+Axio+mYGdzxIki5iwJvRCwKapoZplndXKTrn2nYBuhxW2+fRHa8WYdsm/wn0K+jYMlZhquVjNXyL70/Sym6DkzCtJvveQs2CfcEWQuedjRSGFVFT2jV/s5F8L2TV7nQNVj6dEJSNM5JCdZ//OpiMHMCbPNeSxY9koGplUqFhP54F1WU9x+8xiFjEp8WKxQYKHUtj+ace0lLF4CDGXhFR/0k7Icarpax3hYnvagd2OpZyRJdavKBSs5U7/NPuO6sNhZ2NpzsOiul9Iu8bu3UHCECNKkwN4wF4alTlG9sAAbS4ns4wb9XTajG+OPYoDQZmuJfc71McN6m8KBHEnXU8r4epdR7xREe/w+h2MwtPhLvbxwO592tUxJTAjBgkqhkiG9w0BCRUxFgQUOEXV6IFYGpCSHi0MPHz4b3W0KOQwMTAhMAkGBSsOAwIaBQAEFAjyBCA+mr+5UkKuQ1jGw90ASfbVBAjbvqJJZikDPgICCAA= + PKCS12_CONTENT_WITH_PASSWORD: MIIJYQIBAzCCCScGCSqGSIb3DQEHAaCCCRgEggkUMIIJEDCCA8cGCSqGSIb3DQEHBqCCA7gwggO0AgEAMIIDrQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI2eZRJ7Ar+JQCAggAgIIDgFTbOtkFPjqxAoYRHoq1SbyXKf/NRbBA5AqxQlv9aFVT4VcxUSrMGaSWifX2UjsVWQzn134yoLLbdJ0jTorVD+EuBUmtb3xXbBwLqtFZxwcWodYA5WhPQdDcQo0cD3o1vrsXPQARQR6ISSFnhFjPYdH9cO2LqUKV5pjFhIs2/1VPDS2eY7SWZN52DK3QknSj23S3ZW2s4TFEj/5C4ssbO7cWNWBjjaORnd17FMNgVtcRw8ITmLdGBOpFUwP8wIdiLGrXiyjfMLns74nztRelV30/v0DPlz0pZtOPygi/dy0qpbil3wtOFrtQBLEdvLNmt9ikQgGs3pJBS68eMJLu3jAU6rCIKycq0+E0eMXeHcseyMwgguTj2h4t+E4S7nU11lViBFqkSBKxE28+9fNlPvCsZ4WhQZ6TAW3E/jDy/ZSqmak5V7/khMlRPvtrxz71ivksH0iipPdJJkGi7SDEvETySBETiqIslUmsF0ZYeHR5wIBkB5V8zmi8RRZtpvDGbzuQ22V6sNk2mTDh+BRus7gNCoSGWYXWqNNp1PnznuYCJp9T+0mObcAijE7IQuhpYMeQPF+MUIlG5lmpNouzuygTf++xrKIjzP36DcthnMPeD/8LYWfzkuAeRodjl7Z1G6XLvBD5h+Xlq23WPjMcXUiiWYXxTREAQ1EWUf4A9twGcxHJ5AatbvQY3QUoS4a7LNuy17lF7G+O1SFDtGeHZXHHecaVpuAtqZEYeUpqy6ZzMJXtXE1JNl/UR9TtTordb1V5Pf45JTYKLI+SwxVQbRiTgfhulNc+E3tV1AEELZt4CKmh1OFJoJRtyREMfdVuP4rx7ywIoMYuWw8CRqJ3qSmdwz2kmL2pfJNn6vDfN6rNa+sXWErOJ7naSAKQH2CJfnkCOFxkKfwjbOcNRbnGKah8NyWu6xqlv7Y4xEJYqmPahGHmrSfdAt3mpc5zD74DvetLIczcadKaLH7mp6h98xHayvXh0UE7ERHeskfSPrLxL9A3V1RZXDOtcZFWSKHzokuXMDF9OnrcMYDzYgtzof4ReY2t1ldGF7phYINhDlUNyhzyjwyWQbdkxr/+FtWq8Sbm7o2zMTby48c25gnqD9U8RTAO+bY3oV3dQ4bpAOzWdzVmFjESUFx0xVwbTSJGXdkH4YmD5He+xwxTa0Je0HE5+ui5dbP1gxUY+pCGLOhGMIIFQQYJKoZIhvcNAQcBoIIFMgSCBS4wggUqMIIFJgYLKoZIhvcNAQwKAQKgggTuMIIE6jAcBgoqhkiG9w0BDAEDMA4ECGYAccNFWW6kAgIIAASCBMgbXw69iyx73RGWS3FYeuvF5L1VWXQGrDLdUGwa3SdovXxmP1pKBAb82UuiydXDpKSVCmefeEQTs6ucEFRmXrDIldNanqUwbydy3GSJ+4iNsVQHbFgNppH4e90L7BlLcZ3MzSrVEwxWVMHq+XLqTKf3X3QyrmA3mmF96+Nd+icpDIktd+/h2BHnSXHNP0QfVH27H4DwbMDijttXY0JB+8qP9m25Wn4zkmOPEUhrY4Ptv2I08eHFAuNI0jWUwfRhC4FDbUdwFb0aZjA3Te6uYTsu2zAlmg9HuqsD/Ef/wkBEKZLBkjiXa/niFVrwELXhWZDPBAuo+/1UbzXglsW4QDU4LbUutcs6DLag1vLe40a2LO1ODQm7Zw0bxLkb3f/ry6ZFYvO78XmHo4c/oQf4KPUtM2bLz5q7uOxAx07vHYaU2BVt3NjgiIO5VVKjw0075GdgFxwPvYncv1fsC5jSIkX43GuzEtoBTpJKDYb2nhKbN9XWixwGOhUBTK3WYBhn+uaMJs4l3EgkDtK9tsUs5VQQHawj0WrGS1mQhaBfcyZzv4wSn0d3JUO2CN0e9EReJcQvsEnwUvohilOvjDHHhTq8Kp4XU4jbq7TAKqxs3TOmdoskRykn9oKUPExJVhJQonFT3ietV5BHrnN/QoDCSeOR80ZxvWHrQDz3Hm1ygiHd8LYmN4IjiD8b28ZrCALifWxh0WmIYtLZrUjMZavPh+caWH9IG32fTxV9b1bgJD8vWqscj9jCjeMJvkKQo8PFg1kMAxt1u+bIyktTq42O9qxwGrdqEMeBzXxDJMMaRIH3m9LNZ/P5Nk4/hMURhCZJtRtNfOVTK+Q6kKgsdK2EHcuEnp/qBefZjve+xmitbF1W7C4+B7b2JNBacdIm1nE56DwglT/IUk65JrNFP3rf4c5ic76LCQrvyfLiKCGaqcihM9siLVFPYdrnr8TlGbCFnGbpBqMQA5MtZQaDUug50PJtdxlgfwWH4qliimgchCaZbSTcgN5YTguSe16uUSusHD+r6XdtI0939uDILXJjQMczhIKNw8w0Tn4Z3/g2KlB6cwbtaglnnO4a/USh0cPC1a581byNqeFoMi+mAhqfKkwdDuti4GX7OrhkUOkiRjEUXdcckpmmIsyamH/g1dq3CNFXFNIgRRrzIDo4Opr3Ip2VE/4BDQoo/+Rybzxh8bsHgCEujQf8urGxjGyd2ulHoXzHWhz7pPPuY5UN6dC9WZmOQDVous/1nhYThoLVVc61Rk6d83+Ac7iRg4bY5q/73J4HvPMmrTOOOqqn3wc9Pe5ibEy4tFaYnim4p1ZRm8YcwosZmuFPdsP6G5l5qt6uOyr2+qNpXIBkDpG7I6Ls10O7L3PQAX9zRGfcz6Ds0KtuDrLpaVvhuXpewsBwpo1lmhv9bAa4ppBuWznmKigX+vYojSxd/eCRAtMs+Lx6ppZsYNVhbdEIGKXSGwG98sSTZkoLHBMkUW7S8jpeSCHZWEFBUOPJQzAr5cW1w+RAs33cGUygZ5XEEx4DeW8MnO4lCuP+VDOwu3TAKhzAD+qCyXbLEzWiyL5fq3XL+YJtoAc8Mra9lK6jDqzq4u+PLNoYY+kWTBhCyRZ+PfzcXLry8pxuP5E6VtRgfYcxJTAjBgkqhkiG9w0BCRUxFgQUOEXV6IFYGpCSHi0MPHz4b3W0KOQwMTAhMAkGBSsOAwIaBQAEFBa+SV9FU2UObo+nYKdyt/kZVw6FBAgey4GonFtJ2gICCAA= + JWK_PRIVATE_RSA_PKCS8: "-----BEGIN PRIVATE KEY----- + MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCmN2yzxloN8Qfo + rpTsZ5bafEOpHgg/Tj1+TV8rSWd2KZswxUF0+/+FKmbxPwS0EPGtR2LU4dl8yFSL + EZq637edDgYb2czbj2jGEK3Gqo28ReuZBEapzPIvG6H58qf0WD76FL1SlrMel9UA + WcHloJ9eg2E+4jygHLIUowpo5WAc2o/k0ESppuIt+1kPdb+WwUI8a7OvhWnRhLvN + LaENhJwLag4y7isZTUtwxl/f2nfXncKrttLZeHpj6/DmnDMVhl2NDEOfzHwEbd8n + qPxMYtdCxsofXbXz8dxQlG8zB2ltRAbme8DYZdWoup3CnTngvOT38H9/WVWuY4q4 + eNM0erjzAgMBAAECggEBAJLA5rnHTCV5BRmcYqJjR566DmcXvAJgywxjtb4bPjzm + uT2TO5rVD6J8cI1ZrYZqW2c5WvpIOeThXzu2HF4YPh5tjlkysJu9/6y4dyWr2h47 + warFSrqK191d0WJEq6Oh8mCMxSdRJO7C8W4w0XAzo+Inr0l9KDfZfiWYWg2JT5XI + ubibKKq6P2KxND0UVlYbRsp3fv2loEL9WM5H2bjA/oSbQ4tSJtobpjlsQOHmaxbP + XhvsIV3Dr2ksDuLEhm0vfXnEGRzNk3HV3gLNT741YEP3Sp2ZRjd5U1qFn0D+eWe0 + 4LfDX9auGQCnfjZTHvu4qghX7JxcF40omjmtgkRmZ/kCgYEA4A5nU4ahEww7B65y + uzmGeCUUi8ikWzv1C81pSyUKvKzu8CX41hp9J6oRaLGesKImYiuVQK47FhZ++wwf + pRwHvSxtNU9qXb8ewo+BvadyO1eVrIk4tNV543QlSe7pQAoJGkxCia5rfznAE3In + KF4JvIlchyqs0RQ8wx7lULqwnn0CgYEAven83GM6SfrmO+TBHbjTk6JhP/3CMsIv + mSdo4KrbQNvp4vHO3w1/0zJ3URkmkYGhz2tgPlfd7v1l2I6QkIh4Bumdj6FyFZEB + pxjE4MpfdNVcNINvVj87cLyTRmIcaGxmfylY7QErP8GFA+k4UoH/eQmGKGK44TRz + Yj5hZYGWIC8CgYEAlmmU/AG5SGxBhJqb8wxfNXDPJjf//i92BgJT2Vp4pskBbr5P + GoyV0HbfUQVMnw977RONEurkR6O6gxZUeCclGt4kQlGZ+m0/XSWx13v9t9DIbheA + tgVJ2mQyVDvK4m7aRYlEceFh0PsX8vYDS5o1txgPwb3oXkPTtrmbAGMUBpECgYEA + mxRTU3QDyR2EnCv0Nl0TCF90oliJGAHR9HJmBe//EjuCBbwHfcT8OG3hWOv8vpzo + kQPRl5cQt3NckzX3fs6xlJN4Ai2Hh2zduKFVQ2p+AF2p6Yfahscjtq+GY9cB85Nx + Ly2IXCC0PF++Sq9LOrTE9QV988SJy/yUrAjcZ5MmECkCgYEAldHXIrEmMZVaNwGz + DF9WG8sHj2mOZmQpw9yrjLK9hAsmsNr5LTyqWAqJIYZSwPTYWhY4nu2O0EY9G9uY + iqewXfCKw/UngrJt8Xwfq1Zruz0YY869zPN4GiE9+9rzdZB33RBw8kIOquY3MK74 + FMwCihYx/LiU2YTHkaoJ3ncvtvg= + -----END PRIVATE KEY-----" + JWK_PUB_RSA: '{"kid":"ex","kty":"RSA","key_ops":["sign","verify","wrapKey","unwrapKey","encrypt","decrypt"],"n":"p2VQo8qCfWAZmdWBVaYuYb-a-tWWm78K6Sr9poCvNcmv8rUPSLACxitQWR8gZaSH1DklVkqz-Ed8Cdlf8lkDg4Ex5tkB64jRdC1Uvn4CDpOH6cp-N2s8hTFLqy9_YaDmyQS7HiqthOi9oVjil1VMeWfaAbClGtFt6UnKD0Vb_DvLoWYQSqlhgBArFJi966b4E1pOq5Ad02K8pHBDThlIIx7unibLehhDU6q3DCwNH_OOLx6bgNtmvGYJDd1cywpkLQ3YzNCUPWnfMBJRP3iQP_WI21uP6cvo0DqBPBM4wvVzHbCT0vnIflwkbgEWkq1FprqAitZlop9KjLqzjp9vyQ","e":"AQAB"}' + JWK_PUB_ECDSA: '{"kid":"https://kv-test-mj.vault.azure.net/keys/ec-p-521/e3d0e9c179b54988860c69c6ae172c65","kty":"EC","key_ops":["sign","verify"],"crv":"P-521","x":"AedOAtb7H7Oz1C_cPKI_R4CN_eai5nteY6KFW07FOoaqgQfVCSkQDK22fCOiMT_28c8LZYJRsiIFz_IIbQUW7bXj","y":"AOnchHnmBphIWXvanmMAmcCDkaED6ycW8GsAl9fQ43BMVZTqcTkJYn6vGnhn7MObizmkNSmgZYTwG-vZkIg03HHs"}' + + JWK_PRIV_RSA: '{"kty" : "RSA","kid" : "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df","use" : "sig","n" : "pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGaut-3nQ4GG9nM249oxhCtxqqNvEXrmQRGqczyLxuh-fKn9Fg--hS9UpazHpfVAFnB5aCfXoNhPuI8oByyFKMKaOVgHNqP5NBEqabiLftZD3W_lsFCPGuzr4Vp0YS7zS2hDYScC2oOMu4rGU1LcMZf39p3153Cq7bS2Xh6Y-vw5pwzFYZdjQxDn8x8BG3fJ6j8TGLXQsbKH1218_HcUJRvMwdpbUQG5nvA2GXVqLqdwp054Lzk9_B_f1lVrmOKuHjTNHq48w","e" : "AQAB","d" : "ksDmucdMJXkFGZxiomNHnroOZxe8AmDLDGO1vhs-POa5PZM7mtUPonxwjVmthmpbZzla-kg55OFfO7YcXhg-Hm2OWTKwm73_rLh3JavaHjvBqsVKuorX3V3RYkSro6HyYIzFJ1Ek7sLxbjDRcDOj4ievSX0oN9l-JZhaDYlPlci5uJsoqro_YrE0PRRWVhtGynd-_aWgQv1YzkfZuMD-hJtDi1Im2humOWxA4eZrFs9eG-whXcOvaSwO4sSGbS99ecQZHM2TcdXeAs1PvjVgQ_dKnZlGN3lTWoWfQP55Z7Tgt8Nf1q4ZAKd-NlMe-7iqCFfsnFwXjSiaOa2CRGZn-Q","p" : "4A5nU4ahEww7B65yuzmGeCUUi8ikWzv1C81pSyUKvKzu8CX41hp9J6oRaLGesKImYiuVQK47FhZ--wwfpRwHvSxtNU9qXb8ewo-BvadyO1eVrIk4tNV543QlSe7pQAoJGkxCia5rfznAE3InKF4JvIlchyqs0RQ8wx7lULqwnn0","q" : "ven83GM6SfrmO-TBHbjTk6JhP_3CMsIvmSdo4KrbQNvp4vHO3w1_0zJ3URkmkYGhz2tgPlfd7v1l2I6QkIh4Bumdj6FyFZEBpxjE4MpfdNVcNINvVj87cLyTRmIcaGxmfylY7QErP8GFA-k4UoH_eQmGKGK44TRzYj5hZYGWIC8","dp" : "lmmU_AG5SGxBhJqb8wxfNXDPJjf__i92BgJT2Vp4pskBbr5PGoyV0HbfUQVMnw977RONEurkR6O6gxZUeCclGt4kQlGZ-m0_XSWx13v9t9DIbheAtgVJ2mQyVDvK4m7aRYlEceFh0PsX8vYDS5o1txgPwb3oXkPTtrmbAGMUBpE","dq" : "mxRTU3QDyR2EnCv0Nl0TCF90oliJGAHR9HJmBe__EjuCBbwHfcT8OG3hWOv8vpzokQPRl5cQt3NckzX3fs6xlJN4Ai2Hh2zduKFVQ2p-AF2p6Yfahscjtq-GY9cB85NxLy2IXCC0PF--Sq9LOrTE9QV988SJy_yUrAjcZ5MmECk","qi" : "ldHXIrEmMZVaNwGzDF9WG8sHj2mOZmQpw9yrjLK9hAsmsNr5LTyqWAqJIYZSwPTYWhY4nu2O0EY9G9uYiqewXfCKw_UngrJt8Xwfq1Zruz0YY869zPN4GiE9-9rzdZB33RBw8kIOquY3MK74FMwCihYx_LiU2YTHkaoJ3ncvtvg"}' + JWK_PRIV_ECDSA: '{"kty": "EC","kid": "rie3pHe8u8gjSa0IaJfqk7_iEfHeYfDYx-Bqi7vQc0s","crv": "P-256","x": "fDjg3Nq4jPf8IOZ0277aPVal_8iXySnzLUJAZghUzZM","y": "d863PeyBOK_Q4duiSmWwgIRzi1RPlFZTR-vACMlPg-Q","d": "jJs5xsoHUetdMabtt8H2KyX5T92nGul1chFeMT5hlr0"}' + + TEST_LOWERCASE_STRING: i am a lowercase string + + TEST_YAML_STRING: | + name: test-object + type: example + properties: + key1: value1 + key2: value2 + numbers: + - 1 + - 2 + - 3 + + TEST_JSON_DATA: '{ + "id": "f1bc87fc-99cf-48d2-b289-7cf578152f9a", + "name": "test", + "description": "", + "type": "secret-manager", + "slug": "test-o-bto", + "autoCapitalization": false, + "orgId": "601815be-6884-4ee4-86c7-bfc6415f2123", + "createdAt": "2025-03-26T01:32:39.890Z", + "updatedAt": "2025-03-26T01:32:41.688Z", + "version": 3, + "upgradeStatus": null, + "pitVersionLimit": 10, + "kmsCertificateKeyId": null, + "auditLogsRetentionDays": null, + "_id": "f1bc87fc-99cf-48d2-b289-7cf578152f9a", + "environments": [ + { + "name": "Development", + "slug": "dev", + "id": "cbb62f88-44cb-4c29-975a-871f8d7d303b" + }, + { + "name": "Staging", + "slug": "staging", + "id": "c933a63d-418a-4d5c-a7d1-91b74d3ee2eb" + }, + { + "name": "Production", + "slug": "prod", + "id": "0b70125e-47d5-46e8-a03e-a3105df05d37" + } + ] + }' diff --git a/k8-operator/config/samples/crd/pushsecret/source-secret.yaml b/k8-operator/config/samples/crd/pushsecret/source-secret.yaml new file mode 100644 index 000000000..88fd3e0d2 --- /dev/null +++ b/k8-operator/config/samples/crd/pushsecret/source-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: push-secret-source-secret + namespace: default +stringData: + ENCRYPTION_KEY: secret-encryption-key + API_URL: https://example.com/api + REGION: us-east-1 diff --git a/k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml b/k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml deleted file mode 100644 index 6a3703a95..000000000 --- a/k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: push-secret-demo - namespace: default -stringData: # can also be "data", but needs to be base64 encoded - API_KEY: some-api-key - DATABASE_URL: postgres://127.0.0.1:5432 - ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index 1bcecfba0..bf09c2cfb 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -6,12 +6,13 @@ import ( "errors" "fmt" "strings" - "text/template" + tpl "text/template" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/constants" "github.com/Infisical/infisical/k8-operator/packages/model" + "github.com/Infisical/infisical/k8-operator/packages/template" "github.com/Infisical/infisical/k8-operator/packages/util" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -129,7 +130,7 @@ func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSec for templateKey, userTemplate := range infisicalPushSecret.Spec.Push.Secret.Template.Data { - tmpl, err := template.New("push-secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) + tmpl, err := tpl.New("push-secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate) if err != nil { return nil, fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go index 54afbfa90..f2197b0ed 100644 --- a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go @@ -6,13 +6,14 @@ import ( "errors" "fmt" "strings" - "text/template" + tpl "text/template" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/constants" "github.com/Infisical/infisical/k8-operator/packages/crypto" "github.com/Infisical/infisical/k8-operator/packages/model" + "github.com/Infisical/infisical/k8-operator/packages/template" "github.com/Infisical/infisical/k8-operator/packages/util" "github.com/go-logr/logr" @@ -190,7 +191,7 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeResource(ctx conte } for templateKey, userTemplate := range managedTemplateData.Data { - tmpl, err := template.New("secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) + tmpl, err := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate) if err != nil { return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } @@ -311,7 +312,7 @@ func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context } for templateKey, userTemplate := range managedTemplateData.Data { - tmpl, err := template.New("secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) + tmpl, err := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate) if err != nil { return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } @@ -362,7 +363,7 @@ func (r *InfisicalSecretReconciler) updateInfisicalManagedConfigMap(ctx context. } for templateKey, userTemplate := range managedTemplateData.Data { - tmpl, err := template.New("secret-templates").Funcs(util.InfisicalSecretTemplateFunctions).Parse(userTemplate) + tmpl, err := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate) if err != nil { return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } diff --git a/k8-operator/go.mod b/k8-operator/go.mod index 0731666fa..6ce68ed7f 100644 --- a/k8-operator/go.mod +++ b/k8-operator/go.mod @@ -3,12 +3,15 @@ module github.com/Infisical/infisical/k8-operator go 1.21 require ( + github.com/Masterminds/sprig/v3 v3.3.0 github.com/infisical/go-sdk v0.4.4 + github.com/lestrrat-go/jwx/v2 v2.1.4 github.com/onsi/ginkgo/v2 v2.6.0 github.com/onsi/gomega v1.24.1 k8s.io/apimachinery v0.26.1 k8s.io/client-go v0.26.1 sigs.k8s.io/controller-runtime v0.14.4 + software.sslmate.com/src/go-pkcs12 v0.5.0 ) require ( @@ -16,6 +19,9 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.4.0 // indirect cloud.google.com/go/iam v1.1.11 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/aws/aws-sdk-go-v2 v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.24 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.24 // indirect @@ -29,18 +35,31 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect github.com/aws/smithy-go v1.20.3 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect - golang.org/x/sync v0.7.0 // indirect + golang.org/x/sync v0.10.0 // indirect google.golang.org/api v0.188.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240708141625-4ad9e859172b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect @@ -84,18 +103,18 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.25.0 + golang.org/x/crypto v0.32.0 golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.26.1 k8s.io/apiextensions-apiserver v0.26.1 // indirect k8s.io/component-base v0.26.1 // indirect diff --git a/k8-operator/go.sum b/k8-operator/go.sum index 9037bc4a0..bcecfadc0 100644 --- a/k8-operator/go.sum +++ b/k8-operator/go.sum @@ -38,9 +38,17 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -92,6 +100,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -106,6 +116,8 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -138,6 +150,8 @@ github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/ github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -214,6 +228,8 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -239,10 +255,24 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc= +github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= @@ -250,6 +280,10 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -300,9 +334,17 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -319,8 +361,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -362,8 +404,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -458,8 +500,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -506,16 +548,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -527,8 +569,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -728,3 +770,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kF sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/k8-operator/packages/template/base64.go b/k8-operator/packages/template/base64.go new file mode 100644 index 000000000..3fff06c86 --- /dev/null +++ b/k8-operator/packages/template/base64.go @@ -0,0 +1,18 @@ +package template + +import ( + "encoding/base64" + "fmt" +) + +func decodeBase64ToBytes(encodedString string) string { + decoded, err := base64.StdEncoding.DecodeString(encodedString) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + return string(decoded) +} + +func encodeBase64(plainString string) string { + return base64.StdEncoding.EncodeToString([]byte(plainString)) +} diff --git a/k8-operator/packages/template/json.go b/k8-operator/packages/template/json.go new file mode 100644 index 000000000..e516a609a --- /dev/null +++ b/k8-operator/packages/template/json.go @@ -0,0 +1,32 @@ +package template + +import ( + "encoding/json" + "fmt" +) + +func fromJSON(in []byte) any { + var out any + err := json.Unmarshal(in, &out) + if err != nil { + panic(fmt.Sprintf(errUnmarshalJSON, err)) + } + return out +} + +func toJSON(in any) string { + output, err := json.Marshal(in) + if err != nil { + panic(fmt.Sprintf(errMarshalJSON, err)) + } + return string(output) +} + +func jsonStringToJSON(in string) any { + var out any + err := json.Unmarshal([]byte(in), &out) + if err != nil { + panic(fmt.Sprintf(errUnmarshalJSON, err)) + } + return out +} diff --git a/k8-operator/packages/template/jwk.go b/k8-operator/packages/template/jwk.go new file mode 100644 index 000000000..8dbc8f379 --- /dev/null +++ b/k8-operator/packages/template/jwk.go @@ -0,0 +1,43 @@ +package template + +import ( + "crypto/x509" + "fmt" + + "github.com/lestrrat-go/jwx/v2/jwk" +) + +func jwkPublicKeyPem(jwkjson string) string { + k, err := jwk.ParseKey([]byte(jwkjson)) + if err != nil { + panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err)) + } + var rawkey any + err = k.Raw(&rawkey) + if err != nil { + panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err)) + } + mpk, err := x509.MarshalPKIXPublicKey(rawkey) + if err != nil { + panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err)) + } + return pemEncode(mpk, "PUBLIC KEY") +} + +func jwkPrivateKeyPem(jwkjson string) string { + k, err := jwk.ParseKey([]byte(jwkjson)) + if err != nil { + panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err)) + } + var mpk []byte + var pk any + err = k.Raw(&pk) + if err != nil { + panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err)) + } + mpk, err = x509.MarshalPKCS8PrivateKey(pk) + if err != nil { + panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err)) + } + return pemEncode(mpk, "PRIVATE KEY") +} diff --git a/k8-operator/packages/template/pem.go b/k8-operator/packages/template/pem.go new file mode 100644 index 000000000..f37a9d576 --- /dev/null +++ b/k8-operator/packages/template/pem.go @@ -0,0 +1,98 @@ +package template + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" +) + +const ( + errJunk = "error filtering pem: found junk" + + certTypeLeaf = "leaf" + certTypeIntermediate = "intermediate" + certTypeRoot = "root" +) + +func filterPEM(pemType, input string) string { + data := []byte(input) + var blocks []byte + var block *pem.Block + var rest []byte + for { + block, rest = pem.Decode(data) + data = rest + + if block == nil { + break + } + if !strings.EqualFold(block.Type, pemType) { + continue + } + + var buf bytes.Buffer + err := pem.Encode(&buf, block) + if err != nil { + panic(fmt.Sprintf("[filterPEM] Error: %v", err)) + } + blocks = append(blocks, buf.Bytes()...) + } + + if len(blocks) == 0 && len(rest) != 0 { + panic(fmt.Sprintf("[filterPEM] Error: %v", errJunk)) + } + + return string(blocks) +} + +func filterCertChain(certType, input string) string { + ordered := fetchX509CertChains([]byte(input)) + + switch certType { + case certTypeLeaf: + cert := ordered[0] + if cert.AuthorityKeyId != nil && !bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId) { + return pemEncode(ordered[0].Raw, pemTypeCertificate) + } + case certTypeIntermediate: + if len(ordered) < 2 { + return "" + } + var pemData []byte + for _, cert := range ordered[1:] { + if isRootCertificate(cert) { + break + } + b := &pem.Block{ + Type: pemTypeCertificate, + Bytes: cert.Raw, + } + pemData = append(pemData, pem.EncodeToMemory(b)...) + } + return string(pemData) + case certTypeRoot: + cert := ordered[len(ordered)-1] + if isRootCertificate(cert) { + return pemEncode(cert.Raw, pemTypeCertificate) + } + } + + return "" +} + +func isRootCertificate(cert *x509.Certificate) bool { + return cert.AuthorityKeyId == nil || bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId) +} + +func pemEncode(thing []byte, kind string) string { + buf := bytes.NewBuffer(nil) + err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: thing}) + + if err != nil { + panic(fmt.Sprintf("[pemEncode] Error: %v", err)) + } + + return buf.String() +} diff --git a/k8-operator/packages/template/pem_chain.go b/k8-operator/packages/template/pem_chain.go new file mode 100644 index 000000000..00c4f5d3f --- /dev/null +++ b/k8-operator/packages/template/pem_chain.go @@ -0,0 +1,117 @@ +package template + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" +) + +const ( + errNilCert = "certificate is nil" + errFoundDisjunctCert = "found multiple leaf or disjunct certificates" + errNoLeafFound = "no leaf certificate found" + errChainCycle = "constructing chain resulted in cycle" +) + +type node struct { + cert *x509.Certificate + parent *node + isParent bool +} + +func fetchX509CertChains(data []byte) []*x509.Certificate { + var newCertChain []*x509.Certificate + nodes := pemToNodes(data) + + // at the end of this computation, the output will be a single linked list + // the tail of the list will be the root node (which has no parents) + // the head of the list will be the leaf node (whose parent will be intermediate certs) + // (head) leaf -> intermediates -> root (tail) + for i := range nodes { + for j := range nodes { + // ignore same node to prevent generating a cycle + if i == j { + continue + } + // if ith node AuthorityKeyId is same as jth node SubjectKeyId, jth node was used + // to sign the ith certificate + if bytes.Equal(nodes[i].cert.AuthorityKeyId, nodes[j].cert.SubjectKeyId) { + nodes[j].isParent = true + nodes[i].parent = nodes[j] + break + } + } + } + + var foundLeaf bool + var leaf *node + for i := range nodes { + if !nodes[i].isParent { + if foundLeaf { + panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errFoundDisjunctCert)) + } + // this is the leaf node as it's not a parent for any other node + leaf = nodes[i] + foundLeaf = true + } + } + + if leaf == nil { + panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errNoLeafFound)) + } + + processedNodes := 0 + // iterate through the directed list and append the nodes to new cert chain + for leaf != nil { + processedNodes++ + // ensure we aren't stuck in a cyclic loop + if processedNodes > len(nodes) { + panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errChainCycle)) + } + newCertChain = append(newCertChain, leaf.cert) + leaf = leaf.parent + } + return newCertChain +} + +func fetchCertChains(data []byte) []byte { + var pemData []byte + newCertChain := fetchX509CertChains(data) + + for _, cert := range newCertChain { + b := &pem.Block{ + Type: pemTypeCertificate, + Bytes: cert.Raw, + } + pemData = append(pemData, pem.EncodeToMemory(b)...) + } + return pemData +} + +func pemToNodes(data []byte) []*node { + nodes := make([]*node, 0) + for { + // decode pem to der first + block, rest := pem.Decode(data) + data = rest + + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + panic(fmt.Sprintf("[pemToNodes] Error: %v", err)) + } + + if cert == nil { + panic(fmt.Sprintf("[pemToNodes] Error: %v", errNilCert)) + } + nodes = append(nodes, &node{ + cert: cert, + parent: nil, + isParent: false, + }) + } + return nodes +} diff --git a/k8-operator/packages/template/pkcs12.go b/k8-operator/packages/template/pkcs12.go new file mode 100644 index 000000000..e6763fc46 --- /dev/null +++ b/k8-operator/packages/template/pkcs12.go @@ -0,0 +1,144 @@ +package template + +import ( + "bytes" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + + gopkcs12 "software.sslmate.com/src/go-pkcs12" +) + +func pkcs12keyPass(pass, input string) string { + privateKey, _, _, err := gopkcs12.DecodeChain([]byte(input), pass) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + marshalPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{ + Type: pemTypeKey, + Bytes: marshalPrivateKey, + }); err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + return buf.String() +} + +func parsePrivateKey(block []byte) any { + if k, err := x509.ParsePKCS1PrivateKey(block); err == nil { + return k + } + if k, err := x509.ParsePKCS8PrivateKey(block); err == nil { + return k + } + if k, err := x509.ParseECPrivateKey(block); err == nil { + return k + } + panic("Error: unable to parse private key") +} + +func pkcs12key(input string) string { + return pkcs12keyPass("", input) +} + +func pkcs12certPass(pass, input string) string { + _, certificate, caCerts, err := gopkcs12.DecodeChain([]byte(input), pass) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + var pemData []byte + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{ + Type: pemTypeCertificate, + Bytes: certificate.Raw, + }); err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + pemData = append(pemData, buf.Bytes()...) + + for _, ca := range caCerts { + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{ + Type: pemTypeCertificate, + Bytes: ca.Raw, + }); err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + pemData = append(pemData, buf.Bytes()...) + } + + // try to order certificate chain. If it fails we return + // the unordered raw pem data. + // This fails if multiple leaf or disjunct certs are provided. + ordered := fetchCertChains(pemData) + + return string(ordered) +} + +func pkcs12cert(input string) string { + return pkcs12certPass("", input) +} + +func pemToPkcs12(cert, key string) string { + return pemToPkcs12Pass(cert, key, "") +} + +func pemToPkcs12Pass(cert, key, pass string) string { + certPem, _ := pem.Decode([]byte(cert)) + + parsedCert, err := x509.ParseCertificate(certPem.Bytes) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + return certsToPkcs12(parsedCert, key, nil, pass) +} + +func fullPemToPkcs12(cert, key string) string { + return fullPemToPkcs12Pass(cert, key, "") +} + +func fullPemToPkcs12Pass(cert, key, pass string) string { + certPem, rest := pem.Decode([]byte(cert)) + + parsedCert, err := x509.ParseCertificate(certPem.Bytes) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + caCerts := make([]*x509.Certificate, 0) + for len(rest) > 0 { + caPem, restBytes := pem.Decode(rest) + rest = restBytes + + caCert, err := x509.ParseCertificate(caPem.Bytes) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + caCerts = append(caCerts, caCert) + } + + return certsToPkcs12(parsedCert, key, caCerts, pass) +} + +func certsToPkcs12(cert *x509.Certificate, key string, caCerts []*x509.Certificate, password string) string { + keyPem, _ := pem.Decode([]byte(key)) + parsedKey := parsePrivateKey(keyPem.Bytes) + + pfx, err := gopkcs12.Modern.Encode(parsedKey, cert, caCerts, password) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + + return base64.StdEncoding.EncodeToString(pfx) +} diff --git a/k8-operator/packages/template/template.go b/k8-operator/packages/template/template.go new file mode 100644 index 000000000..e14773179 --- /dev/null +++ b/k8-operator/packages/template/template.go @@ -0,0 +1,77 @@ +package template + +import ( + tpl "text/template" + + "github.com/Masterminds/sprig/v3" +) + +var initialized = false + +var customInfisicalSecretTemplateFunctions = tpl.FuncMap{ + "pkcs12key": pkcs12key, + "pkcs12keyPass": pkcs12keyPass, + "pkcs12cert": pkcs12cert, + "pkcs12certPass": pkcs12certPass, + + "pemToPkcs12": pemToPkcs12, + "pemToPkcs12Pass": pemToPkcs12Pass, + "fullPemToPkcs12": fullPemToPkcs12, + "fullPemToPkcs12Pass": fullPemToPkcs12Pass, + + "filterPEM": filterPEM, + "filterCertChain": filterCertChain, + + "jwkPublicKeyPem": jwkPublicKeyPem, + "jwkPrivateKeyPem": jwkPrivateKeyPem, + + "toYaml": toYAML, + "fromYaml": fromYAML, + + "decodeBase64ToBytes": decodeBase64ToBytes, + "encodeBase64": encodeBase64, + + "fromJson": fromJSON, + "fromJsonStringToJson": jsonStringToJSON, + "toJson": toJSON, +} + +const ( + errParse = "unable to parse template at key %s: %s" + errExecute = "unable to execute template at key %s: %s" + errDecodePKCS12WithPass = "unable to decode pkcs12 with password: %s" + errDecodeCertWithPass = "unable to decode pkcs12 certificate with password: %s" + errParsePrivKey = "unable to parse private key type" + errUnmarshalJSON = "unable to unmarshal json: %s" + errMarshalJSON = "unable to marshal json: %s" + + pemTypeCertificate = "CERTIFICATE" + pemTypeKey = "PRIVATE KEY" +) + +func GetTemplateFunctions() tpl.FuncMap { + // we don't want to run this whole function every time because it's expensive, so we cache the result after the first run + if initialized { + return customInfisicalSecretTemplateFunctions + } + + templates := customInfisicalSecretTemplateFunctions + + sprigFuncs := sprig.TxtFuncMap() + // removed for security reasons + delete(sprigFuncs, "env") + delete(sprigFuncs, "expandenv") + + for k, v := range sprigFuncs { + // make sure we aren't overwriting any of our own functions + _, exists := templates[k] + if !exists { + templates[k] = v + } + } + + customInfisicalSecretTemplateFunctions = templates + initialized = true + + return templates +} diff --git a/k8-operator/packages/template/yaml.go b/k8-operator/packages/template/yaml.go new file mode 100644 index 000000000..5352d5a02 --- /dev/null +++ b/k8-operator/packages/template/yaml.go @@ -0,0 +1,30 @@ +package template + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +func toYAML(v any) string { + data, err := yaml.Marshal(v) + if err != nil { + panic(fmt.Sprintf("Error: %v", err)) + + } + return strings.TrimSuffix(string(data), "\n") +} + +// fromYAML converts a YAML document into a map[string]any. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. +func fromYAML(str string) map[string]any { + mapData := map[string]any{} + + if err := yaml.Unmarshal([]byte(str), &mapData); err != nil { + panic(fmt.Sprintf("Error: %v", err)) + } + return mapData +} diff --git a/k8-operator/packages/util/template.go b/k8-operator/packages/util/template.go deleted file mode 100644 index c8ed98e48..000000000 --- a/k8-operator/packages/util/template.go +++ /dev/null @@ -1,20 +0,0 @@ -package util - -import ( - "encoding/base64" - "fmt" - "text/template" -) - -var InfisicalSecretTemplateFunctions = template.FuncMap{ - "decodeBase64ToBytes": func(encodedString string) string { - decoded, err := base64.StdEncoding.DecodeString(encodedString) - if err != nil { - panic(fmt.Sprintf("Error: %v", err)) - } - return string(decoded) - }, - "encodeBase64": func(plainString string) string { - return base64.StdEncoding.EncodeToString([]byte(plainString)) - }, -} From 09ad1cce96b669308cf0e9e0a714842cec6cbd36 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 29 Mar 2025 03:29:43 +0400 Subject: [PATCH 18/40] fix: incorrect examples --- .../infisical-secret-crd-with-template.yml | 28 +++++++++---------- .../infisicalsecret/infisicalSecretCrd.yaml | 5 +++- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/k8-operator/config/samples/crd/infisicalsecret/infisical-secret-crd-with-template.yml b/k8-operator/config/samples/crd/infisicalsecret/infisical-secret-crd-with-template.yml index c1e0c86b9..06086e93f 100644 --- a/k8-operator/config/samples/crd/infisicalsecret/infisical-secret-crd-with-template.yml +++ b/k8-operator/config/samples/crd/infisicalsecret/infisical-secret-crd-with-template.yml @@ -41,6 +41,9 @@ spec: # Native Kubernetes Auth kubernetesAuth: + serviceAccountRef: + name: + namespace: identityId: serviceAccountTokenPath: "/path/to/your/service-account/token" # Optional, defaults to /var/run/secrets/kubernetes.io/serviceaccount/token @@ -97,18 +100,13 @@ spec: secretsPath: "/path" recursive: true - managedSecretReference: - secretName: managed-secret - secretNamespace: default - template: - includeAllSecrets: true - data: - SSH_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" - BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" - creationPolicy: "Orphan" ## Owner | Orphan - # secretType: kubernetes.io/dockerconfigjson - - # # To be depreciated soon - # tokenSecretReference: - # secretName: service-token - # secretNamespace: default + managedKubeSecretReferences: + - secretName: managed-secret + secretNamespace: default + creationPolicy: "Orphan" ## Owner | Orphan + # secretType: kubernetes.io/dockerconfigjson + template: + includeAllSecrets: true + data: + SSH_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" + BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" diff --git a/k8-operator/config/samples/crd/infisicalsecret/infisicalSecretCrd.yaml b/k8-operator/config/samples/crd/infisicalsecret/infisicalSecretCrd.yaml index 108b86e78..82bb8039c 100644 --- a/k8-operator/config/samples/crd/infisicalsecret/infisicalSecretCrd.yaml +++ b/k8-operator/config/samples/crd/infisicalsecret/infisicalSecretCrd.yaml @@ -41,6 +41,9 @@ spec: # Native Kubernetes Auth kubernetesAuth: + serviceAccountRef: + name: + namespace: identityId: serviceAccountTokenPath: "/path/to/your/service-account/token" # Optional, defaults to /var/run/secrets/kubernetes.io/serviceaccount/token @@ -97,7 +100,7 @@ spec: secretsPath: "/path" recursive: true - managedSecretReferences: + managedKubeSecretReferences: - secretName: managed-secret secretNamespace: default creationPolicy: "Orphan" ## Owner | Orphan From ed94e7a8e780286850a53ce3145c77a80ea30ed7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 29 Mar 2025 03:40:54 +0400 Subject: [PATCH 19/40] fix: edge case delete fails --- .../infisicalpushsecret_controller.go | 2 ++ .../infisicalpushsecret_helper.go | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go index b87c5bcc2..a5526bd6c 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go @@ -65,6 +65,8 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. if err != nil { if errors.IsNotFound(err) { logger.Info("Infisical Push Secret CRD not found") + r.DeleteManagedSecrets(ctx, logger, infisicalPushSecretCRD) + return ctrl.Result{ Requeue: false, }, nil diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index bf09c2cfb..848df3c82 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -486,9 +486,28 @@ func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context resourceVariables := r.getResourceVariables(infisicalPushSecret) infisicalClient := resourceVariables.InfisicalClient + cancelCtx := resourceVariables.CancelCtx + authDetails := resourceVariables.AuthDetails + var err error + + if authDetails.AuthStrategy == "" { + logger.Info("No authentication strategy found. Attempting to authenticate") + authDetails, err = r.handleAuthentication(ctx, infisicalPushSecret, infisicalClient) + r.SetAuthenticatedStatusCondition(ctx, &infisicalPushSecret, err) + + if err != nil { + return fmt.Errorf("unable to authenticate [err=%s]", err) + } + + r.updateResourceVariables(infisicalPushSecret, util.ResourceVariables{ + InfisicalClient: infisicalClient, + CancelCtx: cancelCtx, + AuthDetails: authDetails, + }) + } destination := infisicalPushSecret.Spec.Destination - existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ + existingSecrets, err := resourceVariables.InfisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, From 3ad50a4386332905cb4147a7ac85ea78f9041ceb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 29 Mar 2025 04:45:04 +0400 Subject: [PATCH 20/40] docs(k8s): added better templating docs --- .../kubernetes/infisical-push-secret-crd.mdx | 25 +- .../kubernetes/infisical-secret-crd.mdx | 576 ++++++++---------- .../platforms/kubernetes/overview.mdx | 42 ++ 3 files changed, 307 insertions(+), 336 deletions(-) diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx index 0d1ee3abf..0664f0cd8 100644 --- a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -451,30 +451,7 @@ Using Go templates, you can format, combine, and create new key-value pairs of s To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates. ### Available templating functions - - - **Function name**: encodeBase64 - - **Description**: - Given a string, this function will encode the string as a base64 encoded string. - This function is useful when you want to store a string as a base64 encoded value in Infisical. - - **Returns**: The base64 encoded string. - - **Example**: - The example below assumes that the `PLAIN_KEY` secret is stored in your source secret as a plaintext string. - - ```yaml - push: - secret: - secretName: push-secret-demo - secretNamespace: default - template: - includeAllSecrets: true - data: - PLAIN_KEY: "{{ encodeBase64 .PLAIN_KEY.Value }}" # Will be stored in Infisical as a base64 encoded string - ``` - + Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. ## Applying the InfisicalPushSecret CRD to your cluster diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index 3266fd2b0..f23eb010d 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -654,30 +654,7 @@ To help transform your secrets further, the operator provides a set of built-in ### Available templating functions - - **Function name**: decodeBase64ToBytes - -**Description**: -Given a base64 encoded string, this function will decodes the base64-encoded string. -This function is useful when your secrets are already stored as base64 encoded value in Infisical. - -**Returns**: The decoded base64 string as bytes. - -**Example**: -The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical. -The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`. - -```yaml -managedKubeSecretReferences: -secretName: managed-secret -secretNamespace: default -template: - includeAllSecrets: true - data: - BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" -``` - - +Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. @@ -783,31 +760,8 @@ Using Go templates, you can format, combine, and create new key-value pairs from To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates. ### Available templating functions - - - **Function name**: decodeBase64ToBytes - - **Description**: - Given a base64 encoded string, this function will decodes the base64-encoded string. - This function is useful when your Infisical secrets are already stored as base64 encoded value in Infisical. - - **Returns**: The decoded base64 string as bytes. - - **Example**: - The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical. - The resulting managed config map will contain the decoded value of `BINARY_KEY_BASE64`. - - ```yaml - managedKubeConfigMapReferences: - - configMapName: managed-configmap - configMapNamespace: default - template: - includeAllSecrets: true - data: - BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" - ``` - - + + Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. ## Applying CRD @@ -854,39 +808,39 @@ Here, we will highlight three of the most common ways to utilize it. Learn more This will take all the secrets from your managed secret and expose them to your container -````yaml - envFrom: - - secretRef: - name: managed-secret # managed secret name - ``` + ````yaml + envFrom: + - secretRef: + name: managed-secret # managed secret name + ``` - Example usage in a deployment - ```yaml - apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: + Example usage in a deployment + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: app: nginx - template: - metadata: - labels: + spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret # <- name of managed secret - ports: - - containerPort: 80 -```` + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret # <- name of managed secret + ports: + - containerPort: 80 + ```` @@ -902,91 +856,90 @@ spec: key: SOME_SECRET_KEY # The name of the key which exists in the managed secret ``` -Example usage in a deployment + Example usage in a deployment -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: app: nginx - template: - metadata: - labels: + spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - env: - - name: STRIPE_API_SECRET - valueFrom: - secretKeyRef: - name: managed-secret # <- name of managed secret - key: STRIPE_API_SECRET - ports: - - containerPort: 80 -``` - + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + env: + - name: STRIPE_API_SECRET + valueFrom: + secretKeyRef: + name: managed-secret # <- name of managed secret + key: STRIPE_API_SECRET + ports: + - containerPort: 80 + ``` -This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret -```yaml -volumes: - - name: secrets-volume-name # The name of the volume under which secrets will be stored - secret: - secretName: managed-secret # managed secret name -```` + This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret + ```yaml + volumes: + - name: secrets-volume-name # The name of the volume under which secrets will be stored + secret: + secretName: managed-secret # managed secret name + ```` -You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets + You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets -```yaml -volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true -``` + ```yaml + volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true + ``` -Example usage in a deployment + Example usage in a deployment -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: app: nginx - template: - metadata: - labels: + spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true - ports: - - containerPort: 80 - volumes: - - name: secrets-volume-name - secret: - secretName: managed-secret # <- managed secrets -``` + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true + ports: + - containerPort: 80 + volumes: + - name: secrets-volume-name + secret: + secretName: managed-secret # <- managed secrets + ``` @@ -1021,34 +974,34 @@ secrets.infisical.com/auto-reload: "true" ``` -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx - annotations: - secrets.infisical.com/auto-reload: "true" # <- redeployment annotation -spec: - replicas: 1 - selector: - matchLabels: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: app: nginx - template: - metadata: - labels: + annotations: + secrets.infisical.com/auto-reload: "true" # <- redeployment annotation + spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret - ports: - - containerPort: 80 -``` + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret + ports: + - containerPort: 80 + ``` #### How it works @@ -1069,39 +1022,39 @@ Here, we will highlight three of the most common ways to utilize it. Learn more This will take all the secrets from your managed ConfigMap and expose them to your container -````yaml - envFrom: - - configMapRef: - name: managed-configmap # managed configmap name - ``` + ````yaml + envFrom: + - configMapRef: + name: managed-configmap # managed configmap name + ``` - Example usage in a deployment - ```yaml - apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: + Example usage in a deployment + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: app: nginx - template: - metadata: - labels: + spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - configMapRef: - name: managed-configmap # <- name of managed configmap - ports: - - containerPort: 80 -```` + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - configMapRef: + name: managed-configmap # <- name of managed configmap + ports: + - containerPort: 80 + ```` @@ -1117,92 +1070,91 @@ spec: key: SOME_CONFIG_KEY # The name of the key which exists in the managed configmap ``` -Example usage in a deployment + Example usage in a deployment -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: app: nginx - template: - metadata: - labels: + spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - env: - - name: STRIPE_API_SECRET - valueFrom: - configMapKeyRef: - name: managed-configmap # <- name of managed configmap - key: STRIPE_API_SECRET - ports: - - containerPort: 80 -``` + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + env: + - name: STRIPE_API_SECRET + valueFrom: + configMapKeyRef: + name: managed-configmap # <- name of managed configmap + key: STRIPE_API_SECRET + ports: + - containerPort: 80 + ``` -This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret -```yaml -volumes: - - name: configmaps-volume-name # The name of the volume under which configmaps will be stored - configMap: - name: managed-configmap # managed configmap name -```` + This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret + ```yaml + volumes: + - name: configmaps-volume-name # The name of the volume under which configmaps will be stored + configMap: + name: managed-configmap # managed configmap name + ```` -You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets + You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets -```yaml -volumeMounts: - - name: configmaps-volume-name - mountPath: /etc/config - readOnly: true -``` + ```yaml + volumeMounts: + - name: configmaps-volume-name + mountPath: /etc/config + readOnly: true + ``` -Example usage in a deployment + Example usage in a deployment -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: app: nginx - template: - metadata: - labels: + spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - volumeMounts: - - name: configmaps-volume-name - mountPath: /etc/config - readOnly: true - ports: - - containerPort: 80 - volumes: - - name: configmaps-volume-name - configMap: - name: managed-configmap # <- managed configmap -``` - + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + volumeMounts: + - name: configmaps-volume-name + mountPath: /etc/config + readOnly: true + ports: + - containerPort: 80 + volumes: + - name: configmaps-volume-name + configMap: + name: managed-configmap # <- managed configmap + ``` The definition file of the Kubernetes secret for the CA certificate can be structured like the following: @@ -1228,37 +1180,37 @@ The operator will transfer all labels & annotations present on the `InfisicalSec Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example: -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample - labels: - label-to-be-passed-to-managed-secret: sample-value - annotations: - example.com/annotation-to-be-passed-to-managed-secret: "sample-value" -spec: - .. - authentication: - ... - managedKubeSecretReferences: - ... -``` + ```yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalSecret + metadata: + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" + spec: + .. + authentication: + ... + managedKubeSecretReferences: + ... + ``` -This would result in the following managed secret to be created: + This would result in the following managed secret to be created: -```yaml -apiVersion: v1 -data: ... -kind: Secret -metadata: - annotations: - example.com/annotation-to-be-passed-to-managed-secret: sample-value - secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" - labels: - label-to-be-passed-to-managed-secret: sample-value - name: managed-token - namespace: default -type: Opaque -``` + ```yaml + apiVersion: v1 + data: ... + kind: Secret + metadata: + annotations: + example.com/annotation-to-be-passed-to-managed-secret: sample-value + secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" + labels: + label-to-be-passed-to-managed-secret: sample-value + name: managed-token + namespace: default + type: Opaque + ``` diff --git a/docs/integrations/platforms/kubernetes/overview.mdx b/docs/integrations/platforms/kubernetes/overview.mdx index c4e3f7c64..ca700d777 100644 --- a/docs/integrations/platforms/kubernetes/overview.mdx +++ b/docs/integrations/platforms/kubernetes/overview.mdx @@ -114,6 +114,48 @@ spec: ``` +## Advanced Templating + +With the Infisical Secrets Operator, you can use templating to dynamically generate secrets in Kubernetes. The templating is built on top of [Go templates](https://pkg.go.dev/text/template), which is a powerful and flexible template engine built into Go. + +Please be aware that trying to reference non-existing keys will result in an error. Additionally, each template field is processed individually, which means one template field cannot reference another template field. + + + Please note that templating is currently only supported for the `InfisicalPushSecret` and `InfisicalSecret` CRDs. + + +### Available helper functions + +The Infisical Secrets Operator exposes a wide range of helper functions to make it easier to work with secrets in Kubernetes. + +| Function | Description | Signature | +| -------- | ----------- | --------- | +| `decodeBase64ToBytes` | Given a base64 encoded string, this function will decode the base64-encoded string. | `decodeBase64ToBytes(encodedString string) string` | +| `encodeBase64` | Given a string, this function will encode the string to a base64 encoded string. | `encodeBase64(plainString string) string` | +| `pkcs12key`| Extracts all private keys from a PKCS#12 archive and encodes them in PKCS#8 PEM format. | `pkcs12key(input string) string` | +| `pkcs12keyPass`|Same as pkcs12key. Uses the provided password to decrypt the PKCS#12 archive. | `pkcs12keyPass(pass string, input string) string` | +| `pkcs12cert` | Extracts all certificates from a PKCS#12 archive and orders them if possible. If disjunct or multiple leaf certs are provided they are returned as-is. Sort order: `leaf / intermediate(s) / root`. | `pkcs12cert(input string) string` | +| `pkcs12certPass` | Same as `pkcs12cert`. Uses the provided password to decrypt the PKCS#12 archive. | `pkcs12certPass(pass string, input string) string` | +| `pemToPkcs12` | Takes a PEM encoded certificate and key and creates a base64 encoded PKCS#12 archive. | `pemToPkcs12(cert string, key string) string` | +| `pemToPkcs12Pass` | Same as `pemToPkcs12`. Uses the provided password to encrypt the PKCS#12 archive. | `pemToPkcs12Pass(cert string, key string, pass string) string` | +| `fullPemToPkcs12` | Takes a PEM encoded certificates chain and key and creates a base64 encoded PKCS#12 archive. | `fullPemToPkcs12(cert string, key string) string` | +| `fullPemToPkcs12Pass` | Same as `fullPemToPkcs12`. Uses the provided password to encrypt the PKCS#12 archive. | `fullPemToPkcs12Pass(cert string, key string, pass string) string` | +| `filterPEM` | Filters PEM blocks with a specific type from a list of PEM blocks.. | `filterPEM(pemType string, input string) string` | +| `filterCertChain` | Filters PEM block(s) with a specific certificate type (`leaf`, `intermediate` or `root`) from a certificate chain of PEM blocks (PEM blocks with type `CERTIFICATE`). | `filterCertChain(certType string, input string) string` | +| `jwkPublicKeyPem` | Takes an json-serialized JWK and returns an PEM block of type `PUBLIC KEY` that contains the public key. [See here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey) for details. | `jwkPublicKeyPem(jwkjson string) string` | +| `jwkPrivateKeyPem` | Takes an json-serialized JWK and returns an PEM block of type `PRIVATE KEY` that contains the private key. [See here](https://pkg.go.dev/crypto/x509#MarshalPKCS8PrivateKey) for details. | `jwkPrivateKeyPem(jwkjson string) string` | +| `toYaml` | Takes an interface, marshals it to yaml. It returns a string, even on marshal error (empty string). | `toYaml(v any) string` | +| `fromYaml` | Function converts a YAML document into a `map[string]any`. | `fromYaml(str string) map[string]any` | + +### Sprig functions + +The Infisical Secrets Operator integrates with the [Sprig library](https://github.com/Masterminds/sprig) to provide additional helper functions. + + + We've removed `expandEnv` and `env` from the supported functions for security reasons. + + + ## Global configuration To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap. From 438e2dfa07c4f9744cffc20414452c186d3c4a0d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 29 Mar 2025 04:45:19 +0400 Subject: [PATCH 21/40] fix: removed json functions since they're covered by Sprig lib --- k8-operator/packages/template/json.go | 32 ----------------------- k8-operator/packages/template/template.go | 4 --- 2 files changed, 36 deletions(-) delete mode 100644 k8-operator/packages/template/json.go diff --git a/k8-operator/packages/template/json.go b/k8-operator/packages/template/json.go deleted file mode 100644 index e516a609a..000000000 --- a/k8-operator/packages/template/json.go +++ /dev/null @@ -1,32 +0,0 @@ -package template - -import ( - "encoding/json" - "fmt" -) - -func fromJSON(in []byte) any { - var out any - err := json.Unmarshal(in, &out) - if err != nil { - panic(fmt.Sprintf(errUnmarshalJSON, err)) - } - return out -} - -func toJSON(in any) string { - output, err := json.Marshal(in) - if err != nil { - panic(fmt.Sprintf(errMarshalJSON, err)) - } - return string(output) -} - -func jsonStringToJSON(in string) any { - var out any - err := json.Unmarshal([]byte(in), &out) - if err != nil { - panic(fmt.Sprintf(errUnmarshalJSON, err)) - } - return out -} diff --git a/k8-operator/packages/template/template.go b/k8-operator/packages/template/template.go index e14773179..e4e24ac2a 100644 --- a/k8-operator/packages/template/template.go +++ b/k8-operator/packages/template/template.go @@ -30,10 +30,6 @@ var customInfisicalSecretTemplateFunctions = tpl.FuncMap{ "decodeBase64ToBytes": decodeBase64ToBytes, "encodeBase64": encodeBase64, - - "fromJson": fromJSON, - "fromJsonStringToJson": jsonStringToJSON, - "toJson": toJSON, } const ( From bf6060d35370bf729189a00e0b0e0b698cf31c4b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 29 Mar 2025 04:50:16 +0400 Subject: [PATCH 22/40] fix: make initialization a standalone process to account for concurrent calls --- k8-operator/packages/template/template.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/k8-operator/packages/template/template.go b/k8-operator/packages/template/template.go index e4e24ac2a..d56b3da5d 100644 --- a/k8-operator/packages/template/template.go +++ b/k8-operator/packages/template/template.go @@ -6,8 +6,6 @@ import ( "github.com/Masterminds/sprig/v3" ) -var initialized = false - var customInfisicalSecretTemplateFunctions = tpl.FuncMap{ "pkcs12key": pkcs12key, "pkcs12keyPass": pkcs12keyPass, @@ -45,12 +43,7 @@ const ( pemTypeKey = "PRIVATE KEY" ) -func GetTemplateFunctions() tpl.FuncMap { - // we don't want to run this whole function every time because it's expensive, so we cache the result after the first run - if initialized { - return customInfisicalSecretTemplateFunctions - } - +func InitializeTemplateFunctions() { templates := customInfisicalSecretTemplateFunctions sprigFuncs := sprig.TxtFuncMap() @@ -67,7 +60,8 @@ func GetTemplateFunctions() tpl.FuncMap { } customInfisicalSecretTemplateFunctions = templates - initialized = true - - return templates +} + +func GetTemplateFunctions() tpl.FuncMap { + return customInfisicalSecretTemplateFunctions } From 196c616986a12904cd8d88e656ceb808fcbc5c9f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 29 Mar 2025 04:50:39 +0400 Subject: [PATCH 23/40] Update push-secret-with-template.yaml --- .../samples/crd/pushsecret/push-secret-with-template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml index 805df3a52..a81c2189d 100644 --- a/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml +++ b/k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml @@ -14,7 +14,7 @@ spec: destination: projectId: - environmentSlug: secretsPath: push: From 323d5d2d27bd398f15aa2f113eb98030ef57a0c1 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 31 Mar 2025 13:31:13 -0300 Subject: [PATCH 24/40] Mark TeamCity secrets as password type and Hidden visibility on syncs --- .../integration-auth/integration-sync-secret.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index e5c9d4eb0..6be7397f3 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -3595,7 +3595,10 @@ const syncSecretsTeamCity = async ({ `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, { name: `env.${key}`, - value: secrets[key].value + value: secrets[key].value, + type: { + rawValue: "password display='hidden'" + } }, { headers: { @@ -3652,7 +3655,10 @@ const syncSecretsTeamCity = async ({ `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, { name: `env.${key}`, - value: secrets[key].value + value: secrets[key].value, + type: { + rawValue: "password display='hidden'" + } }, { headers: { From 483f26d863cd5ac63d41ef68e18673aa9dd13e33 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 31 Mar 2025 21:59:24 +0400 Subject: [PATCH 25/40] Update main.go --- k8-operator/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/k8-operator/main.go b/k8-operator/main.go index 79df0d4d8..d2f190595 100644 --- a/k8-operator/main.go +++ b/k8-operator/main.go @@ -22,6 +22,7 @@ import ( infisicalDynamicSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicaldynamicsecret" infisicalPushSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalpushsecret" infisicalSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalsecret" + "github.com/Infisical/infisical/k8-operator/packages/template" //+kubebuilder:scaffold:imports ) @@ -86,6 +87,8 @@ func main() { os.Exit(1) } + template.InitializeTemplateFunctions() + if err = (&infisicalSecretController.InfisicalSecretReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), From 74200bf860ea3f32373732705709835f76fded6e Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 31 Mar 2025 18:20:01 -0300 Subject: [PATCH 26/40] Add clearTimeout on setTimeouts --- .../src/components/permissions/AccessTree/AccessTree.tsx | 4 +++- .../FolderNode/components/AccessTreeSecretPathInput.tsx | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx index 4ba2c718b..ee0667582 100644 --- a/frontend/src/components/permissions/AccessTree/AccessTree.tsx +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -81,12 +81,14 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { }, [selectedPath, environment]); useEffect(() => { + let timer: NodeJS.Timeout; if (initialRender) { - setTimeout(() => { + timer = setTimeout(() => { goToRootNode(); setInitialRender(false); }, 500); } + return () => clearTimeout(timer); }, [nodes, edges, getViewport(), initialRender]); const handleToggleModalView = () => diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx index 1702d1214..76a098ed3 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx @@ -29,9 +29,10 @@ export const AccessTreeSecretPathInput = ({ }; const handleBlur = () => { - setTimeout(() => { + const timeout: NodeJS.Timeout = setTimeout(() => { setIsFocused(false); }, 200); + return () => clearTimeout(timeout); }; useEffect(() => { @@ -50,8 +51,10 @@ export const AccessTreeSecretPathInput = ({ const toggleSearch = () => { setIsExpanded(!isExpanded); if (!isExpanded) { - setTimeout(focusInput, 300); + const timeout: NodeJS.Timeout = setTimeout(focusInput, 300); + return () => clearTimeout(timeout); } + return () => {}; }; return ( From a03c152abfbd8ac721739ab2b1516c7e47929a13 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 1 Apr 2025 02:07:12 +0400 Subject: [PATCH 27/40] fix: add permissions to k8s release --- .github/workflows/release_docker_k8_operator.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release_docker_k8_operator.yaml b/.github/workflows/release_docker_k8_operator.yaml index 4af62df1c..1f894df47 100644 --- a/.github/workflows/release_docker_k8_operator.yaml +++ b/.github/workflows/release_docker_k8_operator.yaml @@ -4,6 +4,10 @@ on: tags: - "infisical-k8-operator/v*.*.*" +permissions: + contents: write + pull-requests: write + jobs: release-image: name: Generate Helm Chart PR From ff4ff0588f8026160dcff369e48ce7af971fe8b4 Mon Sep 17 00:00:00 2001 From: DanielHougaard Date: Mon, 31 Mar 2025 22:10:55 +0000 Subject: [PATCH 28/40] Update Helm chart to version v0.9.0 --- helm-charts/secrets-operator/Chart.yaml | 4 ++-- .../templates/infisicalpushsecret-crd.yaml | 13 +++++++++++++ helm-charts/secrets-operator/values.yaml | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index 087bcd4b6..fea5a1006 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.8.15 +version: v0.9.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.8.15" +appVersion: "v0.9.0" diff --git a/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml index 8491df01c..0c73101a3 100644 --- a/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml +++ b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml @@ -137,6 +137,19 @@ spec: secretNamespace: description: The name space where the Kubernetes Secret is located type: string + template: + properties: + data: + additionalProperties: + type: string + description: The template key values + type: object + includeAllSecrets: + description: This injects all retrieved secrets into the + top level of your template. Secrets defined in the template + will take precedence over the injected ones. + type: boolean + type: object required: - secretName - secretNamespace diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index 897670c46..80ed74356 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: infisical/kubernetes-operator - tag: + tag: v0.9.0 resources: limits: cpu: 500m From 530045aaf2383881280e43469593bd7f2427b3da Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 1 Apr 2025 03:55:29 +0400 Subject: [PATCH 29/40] fix: improved query removed seq scan --- backend/src/ee/services/audit-log/audit-log-dal.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index 053ff2d46..1b8cba462 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -106,16 +106,9 @@ export const auditLogDALFactory = (db: TDbClient) => { } if (secretKey) { void sqlQuery.whereRaw( - `( - "eventMetadata"->>'secretKey' = ? - OR - EXISTS ( - SELECT 1 - FROM jsonb_array_elements("eventMetadata"->'secrets') AS element - WHERE element->>'secretKey' = ? - ) - )`, - [secretKey, secretKey] + `("eventMetadata"->>'secretKey' = ? + OR "eventMetadata"->'secrets' @> ?::jsonb)`, + [secretKey, JSON.stringify([{ secretKey }])] ); } } From a07f168c36fb19777d3a34dd3f7d059c8d69078c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 1 Apr 2025 03:55:49 +0400 Subject: [PATCH 30/40] fix: remove star variant and use outlined button instead --- .../pages/organization/AuditLogsPage/components/LogsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx index 7ac42d40f..1c1baccf0 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx @@ -98,7 +98,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {