diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 6b032c6f0..0cb606cbf 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2379,6 +2379,12 @@ export const AppConnections = { }, LARAVEL_FORGE: { apiToken: "The API token used to authenticate with Laravel Forge." + }, + CHEF: { + serverUrl: "The URL of the Chef server to connect to.", + orgName: "The short name of the Chef organization to connect to.", + userName: "The username used to access Chef.", + privateKey: "The private key used to access Chef." } } }; @@ -2624,6 +2630,10 @@ export const SecretSyncs = { siteId: "The ID of the Netlify site to sync secrets to.", context: "The Netlify context to sync secrets to." }, + CHEF: { + dataBagName: "The name of the Chef data bag to sync secrets to.", + dataBagItemName: "The name of the Chef data bag item to sync secrets to." + }, NORTHFLANK: { projectId: "The ID of the Northflank project to sync secrets to.", projectName: "The name of the Northflank project to sync secrets to.", diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index a3250c6a9..9a4d7f38b 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -48,6 +48,7 @@ import { ChecklyConnectionListItemSchema, SanitizedChecklyConnectionSchema } from "@app/services/app-connection/checkly"; +import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/services/app-connection/chef"; import { CloudflareConnectionListItemSchema, SanitizedCloudflareConnectionSchema @@ -168,7 +169,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedOktaConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options, ...SanitizedRedisConnectionSchema.options, - ...SanitizedLaravelForgeConnectionSchema.options + ...SanitizedLaravelForgeConnectionSchema.options, + ...SanitizedChefConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -212,7 +214,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ OktaConnectionListItemSchema, AzureADCSConnectionListItemSchema, RedisConnectionListItemSchema, - LaravelForgeConnectionListItemSchema + LaravelForgeConnectionListItemSchema, + ChefConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts new file mode 100644 index 000000000..6bfd83391 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts @@ -0,0 +1,85 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateChefConnectionSchema, + SanitizedChefConnectionSchema, + UpdateChefConnectionSchema +} from "@app/services/app-connection/chef"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerChefConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Chef, + server, + sanitizedResponseSchema: SanitizedChefConnectionSchema, + createSchema: CreateChefConnectionSchema, + updateSchema: UpdateChefConnectionSchema + }); + + server.route({ + method: "GET", + url: `/:connectionId/data-bags`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const dataBags = await server.services.appConnection.chef.listDataBags(connectionId, req.permission); + + return dataBags; + } + }); + + server.route({ + method: "GET", + url: `/:connectionId/data-bag-items`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + dataBagName: z.string() + }), + response: { + 200: z + .object({ + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const { dataBagName } = req.query; + const dataBagItems = await server.services.appConnection.chef.listDataBagItems( + connectionId, + dataBagName, + req.permission + ); + + return dataBagItems; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index d8bcdce23..32e9efe4c 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -13,6 +13,7 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router"; +import { registerChefConnectionRouter } from "./chef-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; @@ -86,5 +87,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Chef, + server, + responseSchema: ChefSyncSchema, + createSchema: CreateChefSyncSchema, + updateSchema: UpdateChefSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 2acf2dfc9..d305dc342 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -10,6 +10,7 @@ import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerBitbucketSyncRouter } from "./bitbucket-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerChecklySyncRouter } from "./checkly-sync-router"; +import { registerChefSyncRouter } from "./chef-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; @@ -67,5 +68,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 1e731ed77..1c184a436 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -39,6 +39,7 @@ export enum AppConnection { Okta = "okta", Redis = "redis", LaravelForge = "laravel-forge", + Chef = "chef", Northflank = "northflank" } diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index efc3deb99..3c511fd4d 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -68,6 +68,7 @@ import { } from "./bitbucket"; import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda"; import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly"; +import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef"; import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum"; import { getCloudflareConnectionListItem, @@ -210,7 +211,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getNetlifyConnectionListItem(), getNorthflankConnectionListItem(), getOktaConnectionListItem(), - getRedisConnectionListItem() + getRedisConnectionListItem(), + getChefConnectionListItem() ] .filter((option) => { switch (projectType) { @@ -341,6 +343,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -409,6 +412,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case RenderConnectionMethod.ApiKey: case ChecklyConnectionMethod.ApiKey: return "API Key"; + case ChefConnectionMethod.UserKey: + return "User Key"; case SupabaseConnectionMethod.AccessToken: return "Access Token"; default: @@ -483,7 +488,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Northflank]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported, [AppConnection.Redis]: platformManagedCredentialsNotSupported, - [AppConnection.LaravelForge]: platformManagedCredentialsNotSupported + [AppConnection.LaravelForge]: platformManagedCredentialsNotSupported, + [AppConnection.Chef]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index c684765bd..08ca0c681 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -41,6 +41,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Netlify]: "Netlify", [AppConnection.Okta]: "Okta", [AppConnection.Redis]: "Redis", + [AppConnection.Chef]: "Chef", [AppConnection.Northflank]: "Northflank" }; @@ -85,5 +86,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -330,6 +337,7 @@ export type TAppConnectionInput = { id: string } & ( | TNorthflankConnectionInput | TOktaConnectionInput | TRedisConnectionInput + | TChefConnectionInput ); export type TSqlConnectionInput = @@ -395,7 +403,8 @@ export type TAppConnectionConfig = | TNetlifyConnectionConfig | TNorthflankConnectionConfig | TOktaConnectionConfig - | TRedisConnectionConfig; + | TRedisConnectionConfig + | TChefConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -438,7 +447,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateNetlifyConnectionCredentialsSchema | TValidateNorthflankConnectionCredentialsSchema | TValidateOktaConnectionCredentialsSchema - | TValidateRedisConnectionCredentialsSchema; + | TValidateRedisConnectionCredentialsSchema + | TValidateChefConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/chef/chef-connection-enums.ts b/backend/src/services/app-connection/chef/chef-connection-enums.ts new file mode 100644 index 000000000..58e59c3ba --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-enums.ts @@ -0,0 +1,3 @@ +export enum ChefConnectionMethod { + UserKey = "user-key" +} diff --git a/backend/src/services/app-connection/chef/chef-connection-fns.ts b/backend/src/services/app-connection/chef/chef-connection-fns.ts new file mode 100644 index 000000000..1b35628bc --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-fns.ts @@ -0,0 +1,288 @@ +import { AxiosError } from "axios"; +import crypto from "crypto"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { TChefDataBagItemContent } from "../../secret-sync/chef/chef-sync-types"; +import { AppConnection } from "../app-connection-enums"; +import { ChefConnectionMethod } from "./chef-connection-enums"; +import { + TChefConnection, + TChefConnectionConfig, + TChefDataBag, + TChefDataBagItem, + TGetChefDataBagItem, + TUpdateChefDataBagItem +} from "./chef-connection-types"; + +export const getChefServerUrl = async (serverUrl?: string) => { + const chefServerUrl = serverUrl ? removeTrailingSlash(serverUrl) : IntegrationUrls.CHEF_API_URL; + + await blockLocalAndPrivateIpAddresses(chefServerUrl); + + return chefServerUrl; +}; + +// Helper to ensure private key is in proper PEM format +const formatPrivateKey = (key: string): string => { + let formattedKey = key.trim(); + + // Ensure proper line breaks in PEM format (handle escaped newlines) + formattedKey = formattedKey.replace(/\\n/g, "\n"); + + // Remove any extra whitespace between lines + formattedKey = formattedKey.replace(/\n\s+/g, "\n"); + + // If key doesn't have headers, add PKCS#1 RSA headers + if (!formattedKey.includes("BEGIN")) { + formattedKey = `-----BEGIN RSA PRIVATE KEY-----\n${formattedKey}\n-----END RSA PRIVATE KEY-----`; + } + + // Ensure the key has proper line breaks after headers and before footers + formattedKey = formattedKey.replace(/(-----BEGIN[^-]+-----)\s*/g, "$1\n").replace(/\s*(-----END[^-]+-----)/g, "\n$1"); + + // Remove any duplicate newlines + formattedKey = formattedKey.replace(/\n{3,}/g, "\n\n"); + + return formattedKey; +}; + +const getChefAuthHeaders = ( + method: string, + path: string, + body: string, + userId: string, + privateKey: string, + apiVersion: "1.0" | "1.3" = "1.3" +) => { + const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); // Remove milliseconds from timestamp + + // Calculate content hash based on version + let contentHash: string; + if (apiVersion === "1.3") { + contentHash = crypto.createHash("sha256").update(body).digest("base64"); + } else { + contentHash = crypto.createHash("sha1").update(body).digest("base64"); + } + + // Build canonical request based on version + let canonicalRequest: string; + if (apiVersion === "1.3") { + canonicalRequest = [ + `Method:${method}`, + `Path:${path}`, + `X-Ops-Content-Hash:${contentHash}`, + "X-Ops-Sign:version=1.3", + `X-Ops-Timestamp:${timestamp}`, + `X-Ops-UserId:${userId}`, + "X-Ops-Server-API-Version:1" + ].join("\n"); + } else { + const hashedPath = crypto.createHash("sha1").update(path).digest("base64"); + canonicalRequest = [ + `Method:${method}`, + `Hashed Path:${hashedPath}`, + `X-Ops-Content-Hash:${contentHash}`, + `X-Ops-Timestamp:${timestamp}`, + `X-Ops-UserId:${userId}` + ].join("\n"); + } + + // Format the private key properly + const formattedKey = formatPrivateKey(privateKey); + + // Sign the canonical request + const sign = crypto.createSign(apiVersion === "1.3" ? "RSA-SHA256" : "RSA-SHA1"); + sign.update(canonicalRequest); + const signature = sign.sign(formattedKey, "base64"); + + // Split signature into 60-character chunks + const authHeaders: Record = {}; + const signatureLines = signature.match(/.{1,60}/g) || []; + signatureLines.forEach((line, index) => { + authHeaders[`X-Ops-Authorization-${index + 1}`] = line; + }); + + return { + Accept: "application/json", + "Content-Type": "application/json", + "X-Chef-Version": "14.0.0", + "X-Ops-Timestamp": timestamp, + "X-Ops-UserId": userId, + "X-Ops-Sign": apiVersion === "1.3" ? "version=1.3" : "algorithm=sha1;version=1.0", + "X-Ops-Content-Hash": contentHash, + ...(apiVersion === "1.3" && { "X-Ops-Server-API-Version": "1" }), + ...authHeaders + }; +}; + +export const getChefConnectionListItem = () => { + return { + name: "Chef" as const, + app: AppConnection.Chef as const, + methods: Object.values(ChefConnectionMethod) as [ChefConnectionMethod.UserKey] + }; +}; + +export const validateChefConnectionCredentials = async (config: TChefConnectionConfig) => { + const { credentials: inputCredentials } = config; + + try { + const path = `/organizations/${inputCredentials.orgName}/users/${inputCredentials.userName}`; + + const hostServerUrl = await getChefServerUrl(inputCredentials.serverUrl); + + const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey); + + await request.get(`${hostServerUrl}${path}`, { + headers + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate Chef credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate Chef connection: verify credentials" + }); + } + + return inputCredentials; +}; + +export const listChefDataBags = async (appConnection: TChefConnection): Promise => { + const { + credentials: { serverUrl, userName, privateKey, orgName } + } = appConnection; + + try { + const path = `/organizations/${orgName}/data`; + const body = ""; + + const hostServerUrl = await getChefServerUrl(serverUrl); + + const headers = getChefAuthHeaders("GET", path, body, userName, privateKey); + + const res = await request.get>(`${hostServerUrl}${path}`, { + headers + }); + + return Object.keys(res.data).map((name) => ({ + name + })); + } catch (error) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to list Chef data bags: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to list Chef data bags" + }); + } +}; + +export const listChefDataBagItems = async ( + appConnection: TChefConnection, + dataBagName: string +): Promise => { + const { + credentials: { serverUrl, userName, privateKey, orgName } + } = appConnection; + + try { + const path = `/organizations/${orgName}/data/${dataBagName}`; + const body = ""; + + const hostServerUrl = await getChefServerUrl(serverUrl); + + const headers = getChefAuthHeaders("GET", path, body, userName, privateKey); + + const res = await request.get>(`${hostServerUrl}${path}`, { + headers + }); + + return Object.keys(res.data).map((name) => ({ + name + })); + } catch (error) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to list Chef data bag items: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to list Chef data bag items" + }); + } +}; + +export const getChefDataBagItem = async ({ + serverUrl, + userName, + privateKey, + orgName, + dataBagName, + dataBagItemName +}: TGetChefDataBagItem): Promise => { + try { + const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`; + const body = ""; + + const hostServerUrl = await getChefServerUrl(serverUrl); + + const headers = getChefAuthHeaders("GET", path, body, userName, privateKey); + + const res = await request.get(`${hostServerUrl}${path}`, { + headers + }); + + return res.data; + } catch (error) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to get Chef data bag item: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to get Chef data bag item" + }); + } +}; + +export const updateChefDataBagItem = async ({ + serverUrl, + userName, + privateKey, + orgName, + dataBagName, + dataBagItemName, + data +}: TUpdateChefDataBagItem): Promise => { + try { + const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`; + const body = JSON.stringify(data); + + const hostServerUrl = await getChefServerUrl(serverUrl); + + const headers = getChefAuthHeaders("PUT", path, body, userName, privateKey); + + await request.put(`${hostServerUrl}${path}`, data, { + headers + }); + } catch (error) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to update Chef data bag item: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to update Chef data bag item" + }); + } +}; diff --git a/backend/src/services/app-connection/chef/chef-connection-schemas.ts b/backend/src/services/app-connection/chef/chef-connection-schemas.ts new file mode 100644 index 000000000..e5a3687a2 --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-schemas.ts @@ -0,0 +1,77 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { ChefConnectionMethod } from "./chef-connection-enums"; + +export const ChefConnectionUserKeyCredentialsSchema = z.object({ + serverUrl: z + .string() + .trim() + .url("Valid Chef Server URL required") + .optional() + .describe(AppConnections.CREDENTIALS.CHEF.serverUrl), + orgName: z + .string() + .trim() + .min(1, "Organization name required") + .max(256, "Organization name cannot exceed 256 characters") + .describe(AppConnections.CREDENTIALS.CHEF.orgName), + userName: z + .string() + .trim() + .min(1, "User name required") + .max(256, "User name cannot exceed 256 characters") + .describe(AppConnections.CREDENTIALS.CHEF.userName), + privateKey: z + .string() + .trim() + .min(1, "Private key required") + .max(16384, "Private key cannot exceed 16384 characters") + .describe(AppConnections.CREDENTIALS.CHEF.privateKey) +}); + +const BaseChefConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Chef) }); + +export const ChefConnectionSchema = BaseChefConnectionSchema.extend({ + method: z.literal(ChefConnectionMethod.UserKey), + credentials: ChefConnectionUserKeyCredentialsSchema +}); + +export const SanitizedChefConnectionSchema = z.discriminatedUnion("method", [ + BaseChefConnectionSchema.extend({ + method: z.literal(ChefConnectionMethod.UserKey), + credentials: ChefConnectionUserKeyCredentialsSchema.pick({ serverUrl: true, orgName: true, userName: true }) + }) +]); + +export const ValidateChefConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(ChefConnectionMethod.UserKey).describe(AppConnections.CREATE(AppConnection.Chef).method), + credentials: ChefConnectionUserKeyCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Chef).credentials) + }) +]); + +export const CreateChefConnectionSchema = ValidateChefConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Chef) +); + +export const UpdateChefConnectionSchema = z + .object({ + credentials: ChefConnectionUserKeyCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Chef).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Chef)); + +export const ChefConnectionListItemSchema = z.object({ + name: z.literal("Chef"), + app: z.literal(AppConnection.Chef), + methods: z.nativeEnum(ChefConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/chef/chef-connection-service.ts b/backend/src/services/app-connection/chef/chef-connection-service.ts new file mode 100644 index 000000000..c989e7eaf --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-service.ts @@ -0,0 +1,39 @@ +import { ForbiddenRequestError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listChefDataBagItems, listChefDataBags } from "./chef-connection-fns"; +import { TChefConnection } from "./chef-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const chefConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listDataBags = async (appConnectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor); + + if (!appConnection) { + throw new ForbiddenRequestError({ message: "App connection not found" }); + } + + return listChefDataBags(appConnection); + }; + + const listDataBagItems = async (appConnectionId: string, dataBagName: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor); + + if (!appConnection) { + throw new ForbiddenRequestError({ message: "App connection not found" }); + } + + return listChefDataBagItems(appConnection, dataBagName); + }; + + return { + listDataBags, + listDataBagItems + }; +}; diff --git a/backend/src/services/app-connection/chef/chef-connection-types.ts b/backend/src/services/app-connection/chef/chef-connection-types.ts new file mode 100644 index 000000000..a2da80d3d --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-types.ts @@ -0,0 +1,50 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { TChefDataBagItemContent } from "@app/services/secret-sync/chef"; + +import { AppConnection } from "../app-connection-enums"; +import { + ChefConnectionSchema, + CreateChefConnectionSchema, + ValidateChefConnectionCredentialsSchema +} from "./chef-connection-schemas"; + +export type TChefConnection = z.infer; + +export type TChefConnectionInput = z.infer & { + app: AppConnection.Chef; +}; + +export type TValidateChefConnectionCredentialsSchema = typeof ValidateChefConnectionCredentialsSchema; + +export type TChefConnectionConfig = DiscriminativePick & { + orgName: string; +}; + +export type TChefDataBag = { + name: string; +}; + +export type TChefDataBagItem = { + name: string; +}; + +export type TGetChefDataBagItem = { + serverUrl?: string; + userName: string; + privateKey: string; + orgName: string; + dataBagName: string; + dataBagItemName: string; +}; + +export type TUpdateChefDataBagItem = { + serverUrl?: string; + userName: string; + privateKey: string; + orgName: string; + dataBagName: string; + dataBagItemName: string; + data: TChefDataBagItemContent; +}; diff --git a/backend/src/services/app-connection/chef/index.ts b/backend/src/services/app-connection/chef/index.ts new file mode 100644 index 000000000..e02479d0e --- /dev/null +++ b/backend/src/services/app-connection/chef/index.ts @@ -0,0 +1,4 @@ +export * from "./chef-connection-enums"; +export * from "./chef-connection-fns"; +export * from "./chef-connection-schemas"; +export * from "./chef-connection-types"; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 0608bbd4b..e4e1d3126 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -104,7 +104,8 @@ export enum IntegrationUrls { GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com", GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform", - GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations" + GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations", + CHEF_API_URL = "https://api.chef.io" } export const getIntegrationOptions = async () => { diff --git a/backend/src/services/secret-sync/chef/chef-sync-constants.ts b/backend/src/services/secret-sync/chef/chef-sync-constants.ts new file mode 100644 index 000000000..567b25bbe --- /dev/null +++ b/backend/src/services/secret-sync/chef/chef-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const CHEF_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Chef", + destination: SecretSync.Chef, + connection: AppConnection.Chef, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/chef/chef-sync-fns.ts b/backend/src/services/secret-sync/chef/chef-sync-fns.ts new file mode 100644 index 000000000..7f32b398a --- /dev/null +++ b/backend/src/services/secret-sync/chef/chef-sync-fns.ts @@ -0,0 +1,151 @@ +import { getChefDataBagItem, updateChefDataBagItem } from "@app/services/app-connection/chef"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { + ChefSecret, + TChefDataBagItemContent, + TChefSecret, + TChefSecrets, + TChefSyncWithCredentials, + TGetChefSecrets +} from "./chef-sync-types"; + +const getChefSecretsRaw = async ({ + serverUrl, + userName, + privateKey, + orgName, + dataBagName, + dataBagItemName +}: TGetChefSecrets): Promise => { + const dataBagItem = await getChefDataBagItem({ + serverUrl, + userName, + privateKey, + orgName, + dataBagName, + dataBagItemName + }); + + // Ensure the data bag item has an id field + if (!dataBagItem.id) { + dataBagItem.id = dataBagItemName; + } + + return dataBagItem; +}; + +const getChefSecrets = async (secretSync: TChefSyncWithCredentials): Promise => { + const { + connection, + destinationConfig: { dataBagName, dataBagItemName } + } = secretSync; + + const { serverUrl, userName, privateKey, orgName } = connection.credentials; + + const dataBagItem = await getChefSecretsRaw({ + serverUrl, + orgName, + userName, + privateKey, + dataBagName, + dataBagItemName + }); + + const { id, ...existingSecrets } = dataBagItem; + + // Convert data bag item to key-value pairs + const secrets: ChefSecret[] = []; + Object.entries(existingSecrets).forEach(([key, value]) => { + if (key !== "id" && value !== null && value !== undefined) { + secrets.push({ key, value: String(value) }); + } + }); + + return { id, secrets }; +}; + +const updateChefSecrets = async ( + secretSync: TChefSyncWithCredentials, + id: string, + secrets: Record +) => { + const { + connection, + destinationConfig: { dataBagName, dataBagItemName } + } = secretSync; + + const { serverUrl, userName, privateKey, orgName } = connection.credentials; + + // Chef data bag items must have an 'id' field + const dataBagItemContent: TChefDataBagItemContent = { + id, + ...secrets + }; + + await updateChefDataBagItem({ + serverUrl, + orgName, + userName, + privateKey, + dataBagName, + dataBagItemName, + data: dataBagItemContent + }); +}; + +export const ChefSyncFns = { + async syncSecrets(secretSync: TChefSyncWithCredentials, secretMap: TSecretMap) { + const { + environment, + syncOptions: { disableSecretDeletion, keySchema } + } = secretSync; + + const { id, secrets } = await getChefSecrets(secretSync); + + // Create a map of the existing secrets + const updatedSecretsMap = new Map(secrets.map((secret) => [secret.key, secret.value])); + + // Add/update new secrets + for (const [key, { value }] of Object.entries(secretMap)) { + updatedSecretsMap.set(key, value); + } + + // Delete secrets if not disabled + if (!disableSecretDeletion) { + secrets.forEach((secret) => { + if (!matchesSchema(secret.key, environment?.slug || "", keySchema)) return; + + if (!secretMap[secret.key]) { + updatedSecretsMap.delete(secret.key); + } + }); + } + + // Convert map to object for Chef API + const updatedSecrets = Object.fromEntries(updatedSecretsMap.entries()); + + await updateChefSecrets(secretSync, id, updatedSecrets); + }, + + async getSecrets(secretSync: TChefSyncWithCredentials): Promise { + const { secrets } = await getChefSecrets(secretSync); + + return Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }])); + }, + + async removeSecrets(secretSync: TChefSyncWithCredentials, secretMap: TSecretMap) { + const { id, secrets: existingSecrets } = await getChefSecrets(secretSync); + + const newSecrets = existingSecrets.filter((secret) => !Object.hasOwn(secretMap, secret.key)); + + if (newSecrets.length === existingSecrets.length) { + return; + } + + const updatedSecrets = Object.fromEntries(newSecrets.map((secret) => [secret.key, secret.value])); + + await updateChefSecrets(secretSync, id, updatedSecrets); + } +}; diff --git a/backend/src/services/secret-sync/chef/chef-sync-schemas.ts b/backend/src/services/secret-sync/chef/chef-sync-schemas.ts new file mode 100644 index 000000000..c2e09011e --- /dev/null +++ b/backend/src/services/secret-sync/chef/chef-sync-schemas.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const ChefSyncDestinationConfigSchema = z.object({ + dataBagName: z + .string() + .min(1, "Data Bag Name is required") + .max(256, "Data Bag Name cannot exceed 256 characters") + .describe(SecretSyncs.DESTINATION_CONFIG.CHEF.dataBagName), + dataBagItemName: z + .string() + .min(1, "Data Bag Item Name is required") + .max(256, "Data Bag Item Name cannot exceed 256 characters") + .describe(SecretSyncs.DESTINATION_CONFIG.CHEF.dataBagItemName) +}); + +const ChefSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const ChefSyncSchema = BaseSecretSyncSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Chef), + destinationConfig: ChefSyncDestinationConfigSchema +}); + +export const CreateChefSyncSchema = GenericCreateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({ + destinationConfig: ChefSyncDestinationConfigSchema +}); + +export const UpdateChefSyncSchema = GenericUpdateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({ + destinationConfig: ChefSyncDestinationConfigSchema.optional() +}); + +export const ChefSyncListItemSchema = z.object({ + name: z.literal("Chef"), + connection: z.literal(AppConnection.Chef), + destination: z.literal(SecretSync.Chef), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/chef/chef-sync-types.ts b/backend/src/services/secret-sync/chef/chef-sync-types.ts new file mode 100644 index 000000000..464e0372d --- /dev/null +++ b/backend/src/services/secret-sync/chef/chef-sync-types.ts @@ -0,0 +1,41 @@ +import z from "zod"; + +import { TChefConnection } from "@app/services/app-connection/chef"; + +import { ChefSyncListItemSchema, ChefSyncSchema, CreateChefSyncSchema } from "./chef-sync-schemas"; + +export type TChefSyncListItem = z.infer; + +export type TChefSync = z.infer; + +export type TChefSyncInput = z.infer; + +export type TChefSyncWithCredentials = TChefSync & { + connection: TChefConnection; +}; + +export type TGetChefSecrets = { + serverUrl?: string; + userName: string; + privateKey: string; + orgName: string; + dataBagName: string; + dataBagItemName: string; +}; + +export type TChefSecret = string | number | boolean | null; + +export type TChefDataBagItemContent = { + id: string; + [key: string]: TChefSecret; +}; + +export type TChefSecrets = { + id: string; + secrets: ChefSecret[]; +}; + +export type ChefSecret = { + key: string; + value: string; +}; diff --git a/backend/src/services/secret-sync/chef/index.ts b/backend/src/services/secret-sync/chef/index.ts new file mode 100644 index 000000000..c8599c867 --- /dev/null +++ b/backend/src/services/secret-sync/chef/index.ts @@ -0,0 +1,4 @@ +export * from "./chef-sync-constants"; +export * from "./chef-sync-fns"; +export * from "./chef-sync-schemas"; +export * from "./chef-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index f04247684..835a314ca 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -30,7 +30,8 @@ export enum SecretSync { Netlify = "netlify", Northflank = "northflank", Bitbucket = "bitbucket", - LaravelForge = "laravel-forge" + LaravelForge = "laravel-forge", + Chef = "chef" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 3068e803b..77845535f 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -34,6 +34,7 @@ import { BITBUCKET_SYNC_LIST_OPTION, BitbucketSyncFns } from "./bitbucket"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants"; import { ChecklySyncFns } from "./checkly/checkly-sync-fns"; +import { CHEF_SYNC_LIST_OPTION, ChefSyncFns } from "./chef"; import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants"; import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"; import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers"; @@ -49,8 +50,7 @@ import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; -import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge"; -import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns"; +import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge"; import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify"; import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank"; import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants"; @@ -96,7 +96,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION, [SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION, [SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION, - [SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION + [SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION, + [SecretSync.Chef]: CHEF_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -286,6 +287,8 @@ export const SecretSyncFns = { return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.LaravelForge: return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Chef: + return ChefSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -408,6 +411,9 @@ export const SecretSyncFns = { case SecretSync.LaravelForge: secretMap = await LaravelForgeSyncFns.getSecrets(secretSync); break; + case SecretSync.Chef: + secretMap = await ChefSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -505,6 +511,8 @@ export const SecretSyncFns = { return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.LaravelForge: return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Chef: + return ChefSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 8110cced9..86cb189c3 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -34,7 +34,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Netlify]: "Netlify", [SecretSync.Northflank]: "Northflank", [SecretSync.Bitbucket]: "Bitbucket", - [SecretSync.LaravelForge]: "Laravel Forge" + [SecretSync.LaravelForge]: "Laravel Forge", + [SecretSync.Chef]: "Chef" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -69,7 +70,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Netlify]: AppConnection.Netlify, [SecretSync.Northflank]: AppConnection.Northflank, [SecretSync.Bitbucket]: AppConnection.Bitbucket, - [SecretSync.LaravelForge]: AppConnection.LaravelForge + [SecretSync.LaravelForge]: AppConnection.LaravelForge, + [SecretSync.Chef]: AppConnection.Chef }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -104,7 +106,8 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Netlify]: SecretSyncPlanType.Regular, [SecretSync.Northflank]: SecretSyncPlanType.Regular, [SecretSync.Bitbucket]: SecretSyncPlanType.Regular, - [SecretSync.LaravelForge]: SecretSyncPlanType.Regular + [SecretSync.LaravelForge]: SecretSyncPlanType.Regular, + [SecretSync.Chef]: SecretSyncPlanType.Regular }; export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { @@ -148,7 +151,8 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { [SecretSync.Netlify]: ["accountName", "siteName"], [SecretSync.Northflank]: [], [SecretSync.Bitbucket]: [], - [SecretSync.LaravelForge]: [] + [SecretSync.LaravelForge]: [], + [SecretSync.Chef]: [] }; const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true; @@ -209,5 +213,6 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record + Check out the configuration docs for [Chef + Connections](/integrations/app-connections/chef) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/chef/delete.mdx b/docs/api-reference/endpoints/app-connections/chef/delete.mdx new file mode 100644 index 000000000..43a67f01c --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/chef/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/chef/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/chef/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/chef/get-by-id.mdx new file mode 100644 index 000000000..8461cc553 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/chef/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/chef/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/chef/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/chef/get-by-name.mdx new file mode 100644 index 000000000..f1042abdb --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/chef/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/chef/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/chef/list.mdx b/docs/api-reference/endpoints/app-connections/chef/list.mdx new file mode 100644 index 000000000..dc18436a7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/chef/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/chef" +--- diff --git a/docs/api-reference/endpoints/app-connections/chef/update.mdx b/docs/api-reference/endpoints/app-connections/chef/update.mdx new file mode 100644 index 000000000..780bea960 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/chef/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/chef/{connectionId}" +--- + + + Check out the configuration docs for [Chef + Connections](/integrations/app-connections/chef) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/chef/create.mdx b/docs/api-reference/endpoints/secret-syncs/chef/create.mdx new file mode 100644 index 000000000..61b816d7d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/chef" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/delete.mdx b/docs/api-reference/endpoints/secret-syncs/chef/delete.mdx new file mode 100644 index 000000000..a43d83be1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/chef/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/chef/get-by-id.mdx new file mode 100644 index 000000000..2efeb51e2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/chef/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/chef/get-by-name.mdx new file mode 100644 index 000000000..d6ac030d8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/chef/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/chef/import-secrets.mdx new file mode 100644 index 000000000..734c89a95 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/chef/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/list.mdx b/docs/api-reference/endpoints/secret-syncs/chef/list.mdx new file mode 100644 index 000000000..e38b35e43 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/chef" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/chef/remove-secrets.mdx new file mode 100644 index 000000000..e44df1c3e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/chef/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/chef/sync-secrets.mdx new file mode 100644 index 000000000..8f8eefa04 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/chef/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/chef/update.mdx b/docs/api-reference/endpoints/secret-syncs/chef/update.mdx new file mode 100644 index 000000000..d5c39484d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/chef/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/chef/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 2792e11c9..6d8eb04cc 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -114,6 +114,7 @@ "integrations/app-connections/bitbucket", "integrations/app-connections/camunda", "integrations/app-connections/checkly", + "integrations/app-connections/chef", "integrations/app-connections/cloudflare", "integrations/app-connections/databricks", "integrations/app-connections/digital-ocean", @@ -540,6 +541,7 @@ "integrations/secret-syncs/bitbucket", "integrations/secret-syncs/camunda", "integrations/secret-syncs/checkly", + "integrations/secret-syncs/chef", "integrations/secret-syncs/cloudflare-pages", "integrations/secret-syncs/cloudflare-workers", "integrations/secret-syncs/databricks", @@ -1658,6 +1660,18 @@ "api-reference/endpoints/app-connections/checkly/delete" ] }, + { + "group": "Chef", + "pages": [ + "api-reference/endpoints/app-connections/chef/list", + "api-reference/endpoints/app-connections/chef/available", + "api-reference/endpoints/app-connections/chef/get-by-id", + "api-reference/endpoints/app-connections/chef/get-by-name", + "api-reference/endpoints/app-connections/chef/create", + "api-reference/endpoints/app-connections/chef/update", + "api-reference/endpoints/app-connections/chef/delete" + ] + }, { "group": "Cloudflare", "pages": [ @@ -2149,6 +2163,20 @@ "api-reference/endpoints/secret-syncs/checkly/remove-secrets" ] }, + { + "group": "Chef", + "pages": [ + "api-reference/endpoints/secret-syncs/chef/list", + "api-reference/endpoints/secret-syncs/chef/get-by-id", + "api-reference/endpoints/secret-syncs/chef/get-by-name", + "api-reference/endpoints/secret-syncs/chef/create", + "api-reference/endpoints/secret-syncs/chef/update", + "api-reference/endpoints/secret-syncs/chef/delete", + "api-reference/endpoints/secret-syncs/chef/sync-secrets", + "api-reference/endpoints/secret-syncs/chef/import-secrets", + "api-reference/endpoints/secret-syncs/chef/remove-secrets" + ] + }, { "group": "Cloudflare Pages", "pages": [ @@ -2304,6 +2332,7 @@ "api-reference/endpoints/secret-syncs/laravel-forge/update", "api-reference/endpoints/secret-syncs/laravel-forge/delete", "api-reference/endpoints/secret-syncs/laravel-forge/sync-secrets", + "api-reference/endpoints/secret-syncs/laravel-forge/import-secrets", "api-reference/endpoints/secret-syncs/laravel-forge/remove-secrets" ] }, diff --git a/docs/images/app-connections/chef/app-connection-form.png b/docs/images/app-connections/chef/app-connection-form.png new file mode 100644 index 000000000..9ba5dc8e8 Binary files /dev/null and b/docs/images/app-connections/chef/app-connection-form.png differ diff --git a/docs/images/app-connections/chef/app-connection-generated.png b/docs/images/app-connections/chef/app-connection-generated.png new file mode 100644 index 000000000..c8ab79540 Binary files /dev/null and b/docs/images/app-connections/chef/app-connection-generated.png differ diff --git a/docs/images/app-connections/chef/app-connection-option.png b/docs/images/app-connections/chef/app-connection-option.png new file mode 100644 index 000000000..2a3e46920 Binary files /dev/null and b/docs/images/app-connections/chef/app-connection-option.png differ diff --git a/docs/images/app-connections/chef/chef-connection-details.png b/docs/images/app-connections/chef/chef-connection-details.png new file mode 100644 index 000000000..9fcb7ee0d Binary files /dev/null and b/docs/images/app-connections/chef/chef-connection-details.png differ diff --git a/docs/images/app-connections/chef/chef-dashboard.png b/docs/images/app-connections/chef/chef-dashboard.png new file mode 100644 index 000000000..65195a8d0 Binary files /dev/null and b/docs/images/app-connections/chef/chef-dashboard.png differ diff --git a/docs/images/app-connections/chef/chef-folder.png b/docs/images/app-connections/chef/chef-folder.png new file mode 100644 index 000000000..29f698080 Binary files /dev/null and b/docs/images/app-connections/chef/chef-folder.png differ diff --git a/docs/images/app-connections/chef/download-starter-kit.png b/docs/images/app-connections/chef/download-starter-kit.png new file mode 100644 index 000000000..27f9582ad Binary files /dev/null and b/docs/images/app-connections/chef/download-starter-kit.png differ diff --git a/docs/images/app-connections/chef/extract-starter-kit.png b/docs/images/app-connections/chef/extract-starter-kit.png new file mode 100644 index 000000000..8743dae9b Binary files /dev/null and b/docs/images/app-connections/chef/extract-starter-kit.png differ diff --git a/docs/images/app-connections/chef/private-key-file.png b/docs/images/app-connections/chef/private-key-file.png new file mode 100644 index 000000000..052103d0e Binary files /dev/null and b/docs/images/app-connections/chef/private-key-file.png differ diff --git a/docs/images/app-connections/chef/starter-kit.png b/docs/images/app-connections/chef/starter-kit.png new file mode 100644 index 000000000..b9b9621e4 Binary files /dev/null and b/docs/images/app-connections/chef/starter-kit.png differ diff --git a/docs/images/secret-syncs/chef/select-option.png b/docs/images/secret-syncs/chef/select-option.png new file mode 100644 index 000000000..5fba1e415 Binary files /dev/null and b/docs/images/secret-syncs/chef/select-option.png differ diff --git a/docs/images/secret-syncs/chef/sync-created.png b/docs/images/secret-syncs/chef/sync-created.png new file mode 100644 index 000000000..c6383deb3 Binary files /dev/null and b/docs/images/secret-syncs/chef/sync-created.png differ diff --git a/docs/images/secret-syncs/chef/sync-destination.png b/docs/images/secret-syncs/chef/sync-destination.png new file mode 100644 index 000000000..073631956 Binary files /dev/null and b/docs/images/secret-syncs/chef/sync-destination.png differ diff --git a/docs/images/secret-syncs/chef/sync-details.png b/docs/images/secret-syncs/chef/sync-details.png new file mode 100644 index 000000000..3e8a92008 Binary files /dev/null and b/docs/images/secret-syncs/chef/sync-details.png differ diff --git a/docs/images/secret-syncs/chef/sync-options.png b/docs/images/secret-syncs/chef/sync-options.png new file mode 100644 index 000000000..75f507310 Binary files /dev/null and b/docs/images/secret-syncs/chef/sync-options.png differ diff --git a/docs/images/secret-syncs/chef/sync-review.png b/docs/images/secret-syncs/chef/sync-review.png new file mode 100644 index 000000000..de0f10f8a Binary files /dev/null and b/docs/images/secret-syncs/chef/sync-review.png differ diff --git a/docs/images/secret-syncs/chef/sync-source.png b/docs/images/secret-syncs/chef/sync-source.png new file mode 100644 index 000000000..877b104ef Binary files /dev/null and b/docs/images/secret-syncs/chef/sync-source.png differ diff --git a/docs/integrations/app-connections/chef.mdx b/docs/integrations/app-connections/chef.mdx new file mode 100644 index 000000000..60c49fb1f --- /dev/null +++ b/docs/integrations/app-connections/chef.mdx @@ -0,0 +1,142 @@ +--- +title: "Chef Connection" +description: "Learn how to configure a Chef Connection for Infisical." +--- + +Infisical supports the use of User Private Key to connect with Chef Server. + +Please access your **starter kit** to get all the required information to create a Chef Connection. + + + + If you download a new starter kit, your previous private key/user key will + no longer be valid. Please make sure to update all the places that use the + previous private key. + + + + ![Chef Server User Keys](/images/app-connections/chef/chef-dashboard.png) + + + ![Starter Kit](/images/app-connections/chef/starter-kit.png) + + + ![Download Starter + Kit](/images/app-connections/chef/download-starter-kit.png) + + + ![Extract Starter + Kit](/images/app-connections/chef/extract-starter-kit.png) + + + + + + + Open your starter kit's folder(or `chef-repo`) and navigate to the `.chef` + folder. + + Please make sure you have hidden files visible in your file explorer. + + ![.chef folder](/images/app-connections/chef/chef-folder.png) + + + In the `.chef` folder, you will find a `[your-username].pem` file. ![Private + Key File](/images/app-connections/chef/private-key-file.png) + + **Private Key:** Copy the content of the private key file. + + + + Open the `config.rb` file and copy the content of the file. + ![Config.rb File Content](/images/app-connections/chef/chef-connection-details.png) + + **User Name(1):** The user name of the chef user. + + **Server URL(2):** The server url of the chef server. + + **Organization Name(3):** The organization name of the chef server. + + + + +## Create a Chef Connection in Infisical via UI + + + + + + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click **+ Add Connection** and choose **Chef** Connection from the list of integrations. + ![Select Chef Connection](/images/app-connections/chef/app-connection-option.png) + + + Complete the form by providing: + - A descriptive name for the connection + - An optional description + - Server URL(optional): The URL of the Chef server to connect with (defaults to https://api.chef.io) + - Organization short name + - User name + - Private key: Your Chef user's private key (.pem file) + + ![Chef Connection Modal](/images/app-connections/chef/app-connection-form.png) + + + After submitting the form, your **Chef Connection** will be successfully created and ready to use with your Infisical project. + ![Chef Connection Created](/images/app-connections/chef/app-connection-generated.png) + + + + + + + To create a Chef Connection via API, send a request to the [Create Chef Connection](/api-reference/endpoints/app-connections/chef/create) endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/chef \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-chef-connection", + "method": "user-key", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", + "credentials": { + "orgName": "my-org", + "userName": "my-user", + "privateKey": "your-private-key" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", + "name": "my-chef-connection", + "description": null, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", + "version": 1, + "orgId": "abcdef12-3456-7890-abcd-ef1234567890", + "createdAt": "2025-10-13T10:15:00.000Z", + "updatedAt": "2025-10-13T10:15:00.000Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "d41d8cd98f00b204e9800998ecf8427e", + "app": "chef", + "method": "user-key", + "credentials": { + "orgName": "my-org", + "userName": "my-user", + } + } + } + ``` + + + diff --git a/docs/integrations/secret-syncs/chef.mdx b/docs/integrations/secret-syncs/chef.mdx new file mode 100644 index 000000000..13f3b3474 --- /dev/null +++ b/docs/integrations/secret-syncs/chef.mdx @@ -0,0 +1,154 @@ +--- +title: "Chef Sync" +description: "Learn how to configure a Chef Sync for Infisical." +--- + +**Prerequisites:** + +- Create a [Chef Connection](/integrations/app-connections/chef) + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select Chef](/images/secret-syncs/chef/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/chef/sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/chef/sync-destination.png) + + - **Chef Connection**: The Chef Connection to authenticate with. + - **Data Bag**: The Data Bag to sync secrets to. + - **Data Bag Item**: The Data Bag Item to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/chef/sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Chef when keys conflict. + - **Import Secrets (Prioritize Chef)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Chef over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your Chef Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/chef/sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Chef Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/chef/sync-review.png) + + + If enabled, your Chef Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/chef/sync-created.png) + + + + + + + To create a **Chef Sync**, make an API request to the [Create Chef Sync](/api-reference/endpoints/secret-syncs/chef/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/chef \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-chef-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "sync to chef site", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isEnabled": true, + "isAutoSyncEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "disableSecretDeletion": false + }, + "destinationConfig": { + "dataBagName": "my-data-bag", + "dataBagItemName": "my-data-bag-item" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-chef-sync", + "description": "sync to chef site", + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2025-07-19T12:00:00Z", + "updatedAt": "2025-07-19T12:00:00Z", + "syncStatus": "succeeded", + "lastSyncJobId": "job-1234", + "lastSyncMessage": null, + "lastSyncedAt": "2025-07-19T12:00:00Z", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "disableSecretDeletion": false + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "chef", + "name": "my-chef-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "destination": "chef", + "destinationConfig": { + "dataBagName": "my-data-bag", + "dataBagItemName": "my-data-bag-item" + } + } + } + ``` + + + diff --git a/docs/snippets/AppConnectionsBrowser.jsx b/docs/snippets/AppConnectionsBrowser.jsx index 7761d4bfc..dfe574547 100644 --- a/docs/snippets/AppConnectionsBrowser.jsx +++ b/docs/snippets/AppConnectionsBrowser.jsx @@ -47,6 +47,7 @@ export const AppConnectionsBrowser = () => { {"name": "Auth0", "slug": "auth0", "path": "/integrations/app-connections/auth0", "description": "Learn how to connect your Auth0 to pull secrets from Infisical.", "category": "Identity & Auth"}, {"name": "Okta", "slug": "okta", "path": "/integrations/app-connections/okta", "description": "Learn how to connect your Okta to pull secrets from Infisical.", "category": "Identity & Auth"}, {"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/app-connections/laravel-forge", "description": "Learn how to connect your Laravel Forge to pull secrets from Infisical.", "category": "Hosting"}, + {"name": "Chef", "slug": "chef", "path": "/integrations/app-connections/chef", "description": "Learn how to connect your Chef to pull secrets from Infisical.", "category": "DevOps Tools"}, {"name": "Northflank", "slug": "northflank", "path": "/integrations/app-connections/northflank", "description": "Learn how to connect your Northflank projects to pull secrets from Infisical.", "category": "Hosting"} ].sort(function(a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); diff --git a/docs/snippets/SecretSyncsBrowser.jsx b/docs/snippets/SecretSyncsBrowser.jsx index d71ef1a80..17ff9e97b 100644 --- a/docs/snippets/SecretSyncsBrowser.jsx +++ b/docs/snippets/SecretSyncsBrowser.jsx @@ -38,6 +38,7 @@ export const SecretSyncsBrowser = () => { {"name": "OCI Vault", "slug": "oci-vault", "path": "/integrations/secret-syncs/oci-vault", "description": "Learn how to sync secrets from Infisical to OCI Vault.", "category": "Cloud Providers"}, {"name": "Zabbix", "slug": "zabbix", "path": "/integrations/secret-syncs/zabbix", "description": "Learn how to sync secrets from Infisical to Zabbix.", "category": "Monitoring"}, {"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/secret-syncs/laravel-forge", "description": "Learn how to sync secrets from Infisical to Laravel Forge.", "category": "Hosting"}, + {"name": "Chef", "slug": "chef", "path": "/integrations/secret-syncs/chef", "description": "Learn how to sync secrets from Infisical to Chef.", "category": "DevOps Tools"}, {"name": "Northflank", "slug": "northflank", "path": "/integrations/secret-syncs/northflank", "description": "Learn how to sync secrets from Infisical to Northflank projects.", "category": "Hosting"} ].sort(function(a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); diff --git a/frontend/public/images/integrations/Chef.png b/frontend/public/images/integrations/Chef.png new file mode 100644 index 000000000..8d8886c21 Binary files /dev/null and b/frontend/public/images/integrations/Chef.png differ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ChefSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ChefSyncFields.tsx new file mode 100644 index 000000000..bf5e0908c --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ChefSyncFields.tsx @@ -0,0 +1,93 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { + TChefDataBag, + TChefDataBagItem, + useChefConnectionListDataBagItems, + useChefConnectionListDataBags +} from "@app/hooks/api/appConnections/chef"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const ChefSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Chef } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const dataBagName = useWatch({ name: "destinationConfig.dataBagName", control }); + + const { data: dataBags, isLoading: isDataBagsLoading } = useChefConnectionListDataBags( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + const { data: dataBagItems, isLoading: isDataBagItemsLoading } = + useChefConnectionListDataBagItems(connectionId, dataBagName, { + enabled: Boolean(connectionId && dataBagName) + }); + + const handleChangeConnection = () => { + setValue("destinationConfig.dataBagName", ""); + setValue("destinationConfig.dataBagItemName", ""); + }; + + return ( + <> + + + ( + + dataBag.name === value) ?? null} + onChange={(option) => { + const selectedDataBag = option as SingleValue; + onChange(selectedDataBag?.name ?? ""); + setValue("destinationConfig.dataBagItemName", ""); + }} + options={dataBags} + placeholder="Select a data bag..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.name} + /> + + )} + /> + + ( + + dataBagItem.name === value) ?? null} + onChange={(option) => { + const selectedDataBagItem = option as SingleValue; + onChange(selectedDataBagItem?.name ?? ""); + }} + options={dataBagItems} + placeholder="Select a data bag item..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.name} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index ffad9aa42..50fee8b9b 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -12,6 +12,7 @@ import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { BitbucketSyncFields } from "./BitbucketSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; import { ChecklySyncFields } from "./ChecklySyncFields"; +import { ChefSyncFields } from "./ChefSyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; @@ -104,6 +105,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.LaravelForge: return ; + case SecretSync.Chef: + return ; case SecretSync.Northflank: return ; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index a7fb037de..506c81a21 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -71,6 +71,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Northflank: case SecretSync.Bitbucket: case SecretSync.LaravelForge: + case SecretSync.Chef: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ChefSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ChefSyncReviewFields.tsx new file mode 100644 index 000000000..95f5310a2 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ChefSyncReviewFields.tsx @@ -0,0 +1,18 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const ChefSyncReviewFields = () => { + const { watch } = useFormContext(); + const dataBagName = watch("destinationConfig.dataBagName"); + const dataBagItemName = watch("destinationConfig.dataBagItemName"); + + return ( + <> + {dataBagName} + {dataBagItemName} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 4cc9c7259..940b3c173 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -24,6 +24,7 @@ import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { BitbucketSyncReviewFields } from "./BitbucketSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields"; +import { ChefSyncReviewFields } from "./ChefSyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; @@ -177,6 +178,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.LaravelForge: DestinationFieldsComponent = ; break; + case SecretSync.Chef: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/chef-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/chef-sync-destination-schema.ts new file mode 100644 index 000000000..8d27b616f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/chef-sync-destination-schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const ChefSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Chef), + destinationConfig: z.object({ + dataBagName: z.string().trim().min(1, "Data Bag required"), + dataBagItemName: z.string().trim().min(1, "Data Bag Item required") + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 146c862dc..561191da0 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -9,6 +9,7 @@ import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-desti import { BitbucketSyncDestinationSchema } from "./bitbucket-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { ChecklySyncDestinationSchema } from "./checkly-sync-destination-schema"; +import { ChefSyncDestinationSchema } from "./chef-sync-destination-schema"; import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema"; import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; @@ -65,7 +66,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ NetlifySyncDestinationSchema, NorthflankSyncDestinationSchema, BitbucketSyncDestinationSchema, - LaravelForgeSyncDestinationSchema + LaravelForgeSyncDestinationSchema, + ChefSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 351357d20..1d0d7bec3 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -46,6 +46,7 @@ import { } from "@app/hooks/api/appConnections/types"; import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection"; +import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection"; import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection"; @@ -129,7 +130,8 @@ export const APP_CONNECTION_MAP: Record< name: "Laravel Forge", image: "Laravel Forge.png", size: 65 - } + }, + [AppConnection.Chef]: { name: "Chef", image: "Chef.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -202,6 +204,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case RenderConnectionMethod.ApiKey: case ChecklyConnectionMethod.ApiKey: return { name: "API Key", icon: faKey }; + case ChefConnectionMethod.UserKey: + return { name: "User Key", icon: faKey }; case AzureClientSecretsConnectionMethod.ClientSecret: case AzureAppConfigurationConnectionMethod.ClientSecret: case AzureKeyVaultConnectionMethod.ClientSecret: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index a96af15d4..393aff644 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -121,6 +121,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Netlify]: AppConnection.Netlify, [SecretSync.Northflank]: AppConnection.Northflank, [SecretSync.Bitbucket]: AppConnection.Bitbucket, - [SecretSync.LaravelForge]: AppConnection.LaravelForge + [SecretSync.LaravelForge]: AppConnection.LaravelForge, + [SecretSync.Chef]: AppConnection.Chef }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/chef/index.ts b/frontend/src/hooks/api/appConnections/chef/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/chef/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/chef/queries.tsx b/frontend/src/hooks/api/appConnections/chef/queries.tsx new file mode 100644 index 000000000..f24a39eed --- /dev/null +++ b/frontend/src/hooks/api/appConnections/chef/queries.tsx @@ -0,0 +1,68 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { TChefDataBag, TChefDataBagItem } from "./types"; + +const chefConnectionKeys = { + all: [...appConnectionKeys.all, "chef"] as const, + listDataBags: (connectionId: string) => + [...chefConnectionKeys.all, "data-bags", connectionId] as const, + listDataBagItems: (connectionId: string, dataBagName: string) => + [...chefConnectionKeys.all, "data-bag-items", connectionId, dataBagName] as const +}; + +export const useChefConnectionListDataBags = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TChefDataBag[], + unknown, + TChefDataBag[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: chefConnectionKeys.listDataBags(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/chef/${connectionId}/data-bags` + ); + + return data; + }, + ...options + }); +}; + +export const useChefConnectionListDataBagItems = ( + connectionId: string, + dataBagName: string, + options?: Omit< + UseQueryOptions< + TChefDataBagItem[], + unknown, + TChefDataBagItem[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: chefConnectionKeys.listDataBagItems(connectionId, dataBagName), + queryFn: async () => { + const params = { dataBagName }; + const { data } = await apiRequest.get( + `/api/v1/app-connections/chef/${connectionId}/data-bag-items`, + { params } + ); + + return data; + }, + enabled: Boolean(connectionId && dataBagName), + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/chef/types.ts b/frontend/src/hooks/api/appConnections/chef/types.ts new file mode 100644 index 000000000..fd87d47c7 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/chef/types.ts @@ -0,0 +1,7 @@ +export type TChefDataBag = { + name: string; +}; + +export type TChefDataBagItem = { + name: string; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 4af1dbb27..fba0cbb4b 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -39,5 +39,6 @@ export enum AppConnection { Northflank = "northflank", Okta = "okta", Redis = "redis", - LaravelForge = "laravel-forge" + LaravelForge = "laravel-forge", + Chef = "chef" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 4d4425ef6..1f553f605 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -148,6 +148,10 @@ export type TChecklyConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Checkly; }; +export type TChefConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Chef; +}; + export type TSupabaseConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Supabase; }; @@ -220,7 +224,8 @@ export type TAppConnectionOption = | TNorthflankConnectionOption | TOktaConnectionOption | TAzureAdCsConnectionOption - | TLaravelForgeConnectionOption; + | TLaravelForgeConnectionOption + | TChefConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -264,4 +269,5 @@ export type TAppConnectionOptionMap = { [AppConnection.AzureADCS]: TAzureAdCsConnectionOption; [AppConnection.Redis]: TRedisConnectionOption; [AppConnection.LaravelForge]: TLaravelForgeConnectionOption; + [AppConnection.Chef]: TChefConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/chef-connection.ts b/frontend/src/hooks/api/appConnections/types/chef-connection.ts new file mode 100644 index 000000000..371d42199 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/chef-connection.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum ChefConnectionMethod { + UserKey = "user-key" +} + +export type TChefConnection = TRootAppConnection & { app: AppConnection.Chef } & { + method: ChefConnectionMethod.UserKey; + credentials: { + instanceUrl?: string; + orgName: string; + userName: string; + privateKey: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index d82ad90ec..3e62031b3 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -11,6 +11,7 @@ import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; import { TBitbucketConnection } from "./bitbucket-connection"; import { TCamundaConnection } from "./camunda-connection"; import { TChecklyConnection } from "./checkly-connection"; +import { TChefConnection } from "./chef-connection"; import { TCloudflareConnection } from "./cloudflare-connection"; import { TDatabricksConnection } from "./databricks-connection"; import { TDigitalOceanConnection } from "./digital-ocean"; @@ -53,6 +54,7 @@ export * from "./azure-key-vault-connection"; export * from "./bitbucket-connection"; export * from "./camunda-connection"; export * from "./checkly-connection"; +export * from "./chef-connection"; export * from "./cloudflare-connection"; export * from "./databricks-connection"; export * from "./flyio-connection"; @@ -124,7 +126,8 @@ export type TAppConnection = | TNetlifyConnection | TNorthflankConnection | TOktaConnection - | TRedisConnection; + | TRedisConnection + | TChefConnection; export type TAvailableAppConnection = Pick; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 149759d33..be51805c8 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -30,7 +30,8 @@ export enum SecretSync { Netlify = "netlify", Northflank = "northflank", Bitbucket = "bitbucket", - LaravelForge = "laravel-forge" + LaravelForge = "laravel-forge", + Chef = "chef" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/chef-sync.ts b/frontend/src/hooks/api/secretSyncs/types/chef-sync.ts new file mode 100644 index 000000000..6bd3d7ca2 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/chef-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TChefSync = TRootSecretSync & { + destination: SecretSync.Chef; + destinationConfig: { + dataBagName: string; + dataBagItemName: string; + }; + connection: { + app: AppConnection.Chef; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index bd2e5af4f..b4088728b 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -10,6 +10,7 @@ import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TBitbucketSync } from "./bitbucket-sync"; import { TCamundaSync } from "./camunda-sync"; import { TChecklySync } from "./checkly-sync"; +import { TChefSync } from "./chef-sync"; import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; import { TCloudflareWorkersSync } from "./cloudflare-workers-sync"; import { TDatabricksSync } from "./databricks-sync"; @@ -73,7 +74,8 @@ export type TSecretSync = | TNetlifySync | TNorthflankSync | TBitbucketSync - | TLaravelForgeSync; + | TLaravelForgeSync + | TChefSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index c09a58960..04292d94f 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -20,6 +20,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; import { BitbucketConnectionForm } from "./BitbucketConnectionForm"; import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { ChecklyConnectionForm } from "./ChecklyConnectionForm"; +import { ChefConnectionForm } from "./ChefConnectionForm"; import { CloudflareConnectionForm } from "./CloudflareConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm"; @@ -164,6 +165,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Checkly: return ; + case AppConnection.Chef: + return ; case AppConnection.Supabase: return ; case AppConnection.DigitalOcean: @@ -329,6 +332,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Checkly: return ; + case AppConnection.Chef: + return ; case AppConnection.Supabase: return ; case AppConnection.DigitalOcean: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ChefConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ChefConnectionForm.tsx new file mode 100644 index 000000000..862b4e2bd --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ChefConnectionForm.tsx @@ -0,0 +1,195 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { ChefConnectionMethod, TChefConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TChefConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Chef) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(ChefConnectionMethod.UserKey), + credentials: z.object({ + serverUrl: z.string().trim().url("Valid Chef Server URL required").optional(), + orgName: z.string().trim().min(1, "Organization name required"), + userName: z.string().trim().min(1, "User name required"), + privateKey: z.string().trim().min(1, "Private key required") + }) + }) +]); + +type FormData = z.infer; + +export const ChefConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Chef, + method: ChefConnectionMethod.UserKey + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + + + )} + /> + + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ChefSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ChefSyncDestinationCol.tsx new file mode 100644 index 000000000..cd1b2742e --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ChefSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TChefSync } from "@app/hooks/api/secretSyncs/types/chef-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TChefSync; +}; + +export const ChefSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 4ec6d15ff..985794144 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -9,6 +9,7 @@ import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncC import { BitbucketSyncDestinationCol } from "./BitbucketSyncDestinationCol"; import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol"; +import { ChefSyncDestinationCol } from "./ChefSyncDestinationCol"; import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol"; import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; @@ -103,6 +104,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.LaravelForge: return ; + case SecretSync.Chef: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 48422c41e..de0aad100 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -202,6 +202,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.siteName || destinationConfig.siteId; secondaryText = destinationConfig.orgName || destinationConfig.orgSlug; break; + case SecretSync.Chef: + primaryText = destinationConfig.dataBagName; + secondaryText = destinationConfig.dataBagItemName; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ChefSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ChefSyncDestinationSection.tsx new file mode 100644 index 000000000..c5e230878 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ChefSyncDestinationSection.tsx @@ -0,0 +1,19 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TChefSync } from "@app/hooks/api/secretSyncs/types/chef-sync"; + +type Props = { + secretSync: TChefSync; +}; + +export const ChefSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + return ( + <> + {destinationConfig.dataBagName} + + {destinationConfig.dataBagItemName} + + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 31bc6d4fd..de527d6c8 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -20,6 +20,7 @@ import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinat import { BitbucketSyncDestinationSection } from "./BitbucketSyncDestinationSection"; import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection"; +import { ChefSyncDestinationSection } from "./ChefSyncDestinationSection"; import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection"; import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; @@ -156,6 +157,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.LaravelForge: DestinationComponents = ; break; + case SecretSync.Chef: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 7e6ac8459..a8de5b379 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -74,6 +74,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Northflank: case SecretSync.Bitbucket: case SecretSync.LaravelForge: + case SecretSync.Chef: AdditionalSyncOptionsComponent = null; break; default: