From 968284956b43c43392f8014b58f5331b935169a8 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 30 Oct 2025 02:17:37 +0530 Subject: [PATCH] feat: adds app connection WIP --- backend/src/lib/api-docs/constants.ts | 6 + .../app-connection-router.ts | 7 +- .../chef-connection-router.ts | 84 ++++++ .../routes/v1/app-connection-routers/index.ts | 4 +- .../app-connection/app-connection-enums.ts | 3 +- .../app-connection/app-connection-fns.ts | 12 +- .../app-connection/app-connection-maps.ts | 6 +- .../app-connection/app-connection-service.ts | 8 +- .../app-connection/app-connection-types.ts | 14 +- .../chef/chef-connection-enums.ts | 3 + .../chef/chef-connection-fns.ts | 264 ++++++++++++++++++ .../chef/chef-connection-schemas.ts | 77 +++++ .../chef/chef-connection-service.ts | 40 +++ .../chef/chef-connection-types.ts | 32 +++ .../src/services/app-connection/chef/index.ts | 4 + .../integration-auth/integration-list.ts | 3 +- frontend/public/images/integrations/Chef.png | Bin 0 -> 4411 bytes frontend/src/helpers/appConnections.ts | 6 +- .../src/hooks/api/appConnections/enums.ts | 3 +- .../api/appConnections/types/app-options.ts | 8 +- .../appConnections/types/chef-connection.ts | 16 ++ .../hooks/api/appConnections/types/index.ts | 5 +- .../AppConnectionForm/AppConnectionForm.tsx | 5 + .../AppConnectionForm/ChefConnectionForm.tsx | 197 +++++++++++++ 24 files changed, 789 insertions(+), 18 deletions(-) create mode 100644 backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts create mode 100644 backend/src/services/app-connection/chef/chef-connection-enums.ts create mode 100644 backend/src/services/app-connection/chef/chef-connection-fns.ts create mode 100644 backend/src/services/app-connection/chef/chef-connection-schemas.ts create mode 100644 backend/src/services/app-connection/chef/chef-connection-service.ts create mode 100644 backend/src/services/app-connection/chef/chef-connection-types.ts create mode 100644 backend/src/services/app-connection/chef/index.ts create mode 100644 frontend/public/images/integrations/Chef.png create mode 100644 frontend/src/hooks/api/appConnections/types/chef-connection.ts create mode 100644 frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ChefConnectionForm.tsx diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index dde9b4e35..cd0782483 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2376,6 +2376,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 name of the Chef organization to connect to.", + userName: "The username used to access Chef.", + privateKey: "The private key used to access Chef." } } }; 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 c799ef0f0..29f22621f 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 @@ -163,7 +164,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedOktaConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options, ...SanitizedRedisConnectionSchema.options, - ...SanitizedLaravelForgeConnectionSchema.options + ...SanitizedLaravelForgeConnectionSchema.options, + ...SanitizedChefConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -206,7 +208,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..d57bd354f --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/chef-connection-router.ts @@ -0,0 +1,84 @@ +// 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({ + // id: z.string(), + // 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-bags/:dataBagName/items`, + // config: { + // rateLimit: readLimit + // }, + // schema: { + // params: z.object({ + // connectionId: z.string().uuid(), + // dataBagName: z.string() + // }), + // response: { + // 200: z + // .object({ + // id: z.string(), + // name: z.string() + // }) + // .array() + // } + // }, + // onRequest: verifyAuth([AuthMode.JWT]), + // handler: async (req) => { + // const { connectionId, dataBagName } = req.params; + // 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 2e3da4420..a24e72766 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"; @@ -84,5 +85,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record [value, key]) @@ -204,7 +205,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getDigitalOceanConnectionListItem(), getNetlifyConnectionListItem(), getOktaConnectionListItem(), - getRedisConnectionListItem() + getRedisConnectionListItem(), + getChefConnectionListItem() ] .filter((option) => { switch (projectType) { @@ -334,7 +336,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); @@ -401,6 +404,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: @@ -474,7 +479,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Netlify]: 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 c01d9d1b4..93cd18f82 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -40,7 +40,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.DigitalOcean]: "DigitalOcean App Platform", [AppConnection.Netlify]: "Netlify", [AppConnection.Okta]: "Okta", - [AppConnection.Redis]: "Redis" + [AppConnection.Redis]: "Redis", + [AppConnection.Chef]: "Chef" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -83,5 +84,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -322,6 +329,7 @@ export type TAppConnectionInput = { id: string } & ( | TNetlifyConnectionInput | TOktaConnectionInput | TRedisConnectionInput + | TChefConnectionInput ); export type TSqlConnectionInput = @@ -386,7 +394,8 @@ export type TAppConnectionConfig = | TDigitalOceanConnectionConfig | TNetlifyConnectionConfig | TOktaConnectionConfig - | TRedisConnectionConfig; + | TRedisConnectionConfig + | TChefConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -428,7 +437,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateDigitalOceanCredentialsSchema | TValidateNetlifyConnectionCredentialsSchema | 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..fe9e917b0 --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-fns.ts @@ -0,0 +1,264 @@ +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 { AppConnection } from "../app-connection-enums"; +import { ChefConnectionMethod } from "./chef-connection-enums"; +import { TChefConnection, TChefConnectionConfig, TChefDataBag } 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; +}; + +// Chef API authentication helper +const getChefAuthHeaders = ( + method: string, + path: string, + body: string, + userId: string, + privateKey: string, + timestamp: string +) => { + const hashedPath = crypto.createHash("sha256").update(path).digest("base64"); + const hashedBody = crypto.createHash("sha256").update(body).digest("base64"); + + console.log("1", { + method, + path, + body, + userId, + privateKey, + timestamp + }); + const canonicalRequest = [ + `Method:${method}`, + `Hashed Path:${hashedPath}`, + `X-Ops-Content-Hash:${hashedBody}`, + `X-Ops-Timestamp:${timestamp}`, + `X-Ops-UserId:${userId}` + ].join("\n"); + + // Format the private key properly + const formattedKey = formatPrivateKey(privateKey); + + console.log("Chef Auth Debug - Key format:", { + formattedKey, + hasHeader: formattedKey.includes("BEGIN"), + keyType: formattedKey.match(/-----BEGIN ([^-]+)-----/)?.[1], + keyLength: formattedKey.length, + startsCorrectly: formattedKey.startsWith("-----BEGIN"), + endsCorrectly: formattedKey.endsWith("-----") + }); + + // Create key object with proper format + let keyObject; + try { + keyObject = crypto.createPrivateKey({ + key: Buffer.from(formattedKey), + format: "pem" + }); + } catch (error) { + console.error("Failed to create private key:", error); + console.error("Key preview (first 100 chars):", formattedKey.substring(0, 100)); + throw new Error(`Invalid private key format: ${error instanceof Error ? error.message : "Unknown error"}`); + } + + // Sign using the key object with SHA-256 (FIPS-compliant) + const signature = crypto.sign("sha256", Buffer.from(canonicalRequest), { + key: keyObject, + padding: crypto.constants.RSA_PKCS1_PADDING + }); + + const signatureBase64 = signature.toString("base64"); + + console.log("4", { signature, signatureBase64 }); + + // Split signature into 60 character chunks as required by Chef API + const signatureLines: string[] = []; + for (let i = 0; i < signatureBase64.length; i += 60) { + signatureLines.push(signatureBase64.substring(i, i + 60)); + } + + console.log("5", signatureLines); + const headers: Record = { + "X-Ops-Sign": "algorithm=sha256;version=1.3", + "X-Ops-UserId": userId, + "X-Ops-Timestamp": timestamp, + "X-Ops-Content-Hash": hashedBody, + "X-Chef-Version": "12.0.2", + "X-Ops-Server-API-Version": "1" + }; + + console.log("6", headers); + + signatureLines.forEach((line, index) => { + headers[`X-Ops-Authorization-${index + 1}`] = line; + }); + + console.log("7", headers); + + return headers; +}; + +export const getChefConnectionListItem = () => { + return { + name: "Chef" as const, + app: AppConnection.Chef as const, + methods: Object.values(ChefConnectionMethod) as [ChefConnectionMethod.UserKey] + }; +}; + +export const listChefDataBags = async (appConnection: TChefConnection): Promise => { + const { + credentials: { serverUrl, userName, privateKey } + } = appConnection; + + try { + const path = "/data"; + const timestamp = `${new Date().toISOString().slice(0, -5)}Z`; + const body = ""; + + const hostServerUrl = await getChefServerUrl(serverUrl); + + const headers = getChefAuthHeaders("GET", path, body, userName, privateKey, timestamp); + + const res = await request.get>(`${hostServerUrl}${path}`, { + headers: { + ...headers, + Accept: "application/json" + } + }); + + // Chef returns data bags as an object with keys being data bag names + return Object.keys(res.data).map((name) => ({ + name, + id: name + })); + } catch (error) { + if (error instanceof AxiosError) { + // throw new BadRequestError({ + // message: `Failed to list Chef data bags: ${error.response?.data?.error || error.message}` + // }); + } + throw new BadRequestError({ + message: "Unable to list Chef data bags" + }); + } +}; + +export const validateChefConnectionCredentials = async (config: TChefConnectionConfig) => { + const { credentials: inputCredentials } = config; + + try { + const path = `/organizations/${inputCredentials.orgName}/users/${inputCredentials.userName}`; + const timestamp = `${new Date().toISOString().slice(0, -5)}Z`; + const body = ""; + + const hostServerUrl = await getChefServerUrl(inputCredentials.serverUrl); + + console.log("Chef validate - attempting connection to:", hostServerUrl); + const headers = getChefAuthHeaders( + "GET", + path, + body, + inputCredentials.userName, + inputCredentials.privateKey, + timestamp + ); + + const data = await request.get(`${hostServerUrl}${path}`, { + headers: { + ...headers, + Accept: "application/json" + } + }); + console.log("Chef validate - data:", data); + } catch (error: unknown) { + if (error instanceof AxiosError) { + const errorData = error.response?.data as { error?: string } | undefined; + console.log("Chef validate - error:", errorData); + throw new BadRequestError({ + message: `Failed to validate Chef credentials: ${errorData?.error || error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate Chef connection: verify credentials" + }); + } + + return inputCredentials; +}; + +export const listChefDataBagItems = async ( + appConnection: TChefConnection, + dataBagName: string +): Promise => { + const { + credentials: { serverUrl, userName, privateKey } + } = appConnection; + + try { + const path = `/data/${dataBagName}`; + const timestamp = `${new Date().toISOString().slice(0, -5)}Z`; + const body = ""; + + const hostServerUrl = await getChefServerUrl(serverUrl); + + const headers = getChefAuthHeaders("GET", path, body, userName, privateKey, timestamp); + + const res = await request.get>(`${hostServerUrl}${path}`, { + headers: { + ...headers, + Accept: "application/json" + } + }); + + // Chef returns data bag items as an object with keys being item names + return Object.keys(res.data).map((name) => ({ + name, + id: name + })); + } catch (error) { + if (error instanceof AxiosError) { + // throw new BadRequestError({ + // message: `Failed to list Chef data bag items: ${error.response?.data?.error || error.message}` + // }); + } + throw new BadRequestError({ + message: "Unable to list Chef data bag items" + }); + } +}; 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..8357a8e4d --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-service.ts @@ -0,0 +1,40 @@ +import { ForbiddenRequestError } from "@app/lib/errors"; +// import { logger } from "@app/lib/logger"; +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..7996026af --- /dev/null +++ b/backend/src/services/app-connection/chef/chef-connection-types.ts @@ -0,0 +1,32 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +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 & { + orgId: string; +}; + +export type TChefDataBag = { + name: string; + id: string; +}; + +export type TChefDataBagItem = { + name: string; + id: string; +}; 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/frontend/public/images/integrations/Chef.png b/frontend/public/images/integrations/Chef.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8886c218ac70114639ca70b889982d11664851 GIT binary patch literal 4411 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 { @@ -199,6 +201,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/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 66fed8a5d..c58fb9194 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -38,5 +38,6 @@ export enum AppConnection { Netlify = "netlify", 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 797e7a7c6..edf26d2e1 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; }; @@ -215,7 +219,8 @@ export type TAppConnectionOption = | TNetlifyConnectionOption | TOktaConnectionOption | TAzureAdCsConnectionOption - | TLaravelForgeConnectionOption; + | TLaravelForgeConnectionOption + | TChefConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -258,4 +263,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 fd840d5de..2664a462c 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -40,6 +40,7 @@ import { TTerraformCloudConnection } from "./terraform-cloud-connection"; import { TVercelConnection } from "./vercel-connection"; import { TWindmillConnection } from "./windmill-connection"; import { TZabbixConnection } from "./zabbix-connection"; +import { TChefConnection } from "./chef-connection"; export * from "./1password-connection"; export * from "./auth0-connection"; @@ -79,6 +80,7 @@ export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; export * from "./zabbix-connection"; +export * from "./chef-connection"; export type TAppConnection = | TAwsConnection @@ -120,7 +122,8 @@ export type TAppConnection = | TDigitalOceanConnection | TNetlifyConnection | TOktaConnection - | TRedisConnection; + | TRedisConnection + | TChefConnection; export type TAvailableAppConnection = Pick; 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 c4115a4b7..eee0cd84f 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"; @@ -163,6 +164,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Checkly: return ; + case AppConnection.Chef: + return ; case AppConnection.Supabase: return ; case AppConnection.DigitalOcean: @@ -326,6 +329,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..d56a7523b --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ChefConnectionForm.tsx @@ -0,0 +1,197 @@ +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; + + console.log("form", form.formState.errors, form.getValues()); + + return ( + +
+ {!isUpdate && } + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + + + )} + /> + + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +};