mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 18:29:06 +00:00
feat: adds app connection WIP
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// });
|
||||
};
|
||||
@@ -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<AppConnection, (server:
|
||||
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter,
|
||||
[AppConnection.Chef]: registerChefConnectionRouter
|
||||
};
|
||||
|
||||
@@ -38,7 +38,8 @@ export enum AppConnection {
|
||||
Netlify = "netlify",
|
||||
Okta = "okta",
|
||||
Redis = "redis",
|
||||
LaravelForge = "laravel-forge"
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -142,6 +142,7 @@ import {
|
||||
WindmillConnectionMethod
|
||||
} from "./windmill";
|
||||
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix";
|
||||
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
|
||||
|
||||
const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries(
|
||||
Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [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 (
|
||||
|
||||
@@ -40,7 +40,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[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<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -83,5 +84,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Chef]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -67,6 +67,8 @@ import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
||||
import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateChefConnectionCredentialsSchema } from "./chef";
|
||||
import { chefConnectionService } from "./chef/chef-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
@@ -171,7 +173,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -877,6 +880,7 @@ export const appConnectionServiceFactory = ({
|
||||
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
|
||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById),
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
|
||||
chef: chefConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -233,6 +233,12 @@ import {
|
||||
TZabbixConnectionConfig,
|
||||
TZabbixConnectionInput
|
||||
} from "./zabbix";
|
||||
import {
|
||||
TChefConnection,
|
||||
TChefConnectionConfig,
|
||||
TChefConnectionInput,
|
||||
TValidateChefConnectionCredentialsSchema
|
||||
} from "./chef";
|
||||
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
@@ -275,6 +281,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TNetlifyConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TChefConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum ChefConnectionMethod {
|
||||
UserKey = "user-key"
|
||||
}
|
||||
@@ -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<string, string> = {
|
||||
"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<TChefDataBag[]> => {
|
||||
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<Record<string, string>>(`${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<TChefDataBag[]> => {
|
||||
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<Record<string, string>>(`${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"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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<TChefConnection>;
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -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<typeof ChefConnectionSchema>;
|
||||
|
||||
export type TChefConnectionInput = z.infer<typeof CreateChefConnectionSchema> & {
|
||||
app: AppConnection.Chef;
|
||||
};
|
||||
|
||||
export type TValidateChefConnectionCredentialsSchema = typeof ValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TChefConnectionConfig = DiscriminativePick<TChefConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TChefDataBag = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TChefDataBagItem = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./chef-connection-enums";
|
||||
export * from "./chef-connection-fns";
|
||||
export * from "./chef-connection-schemas";
|
||||
export * from "./chef-connection-types";
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user