Merge pull request #4784 from Infisical/feat/chef-data-bag-app-connection-secret-sync

[ENG-4034] feat: Adds chef data bag app connection and secret sync
This commit is contained in:
Piyush Gupta
2025-11-01 14:17:09 +05:30
committed by GitHub
94 changed files with 1818 additions and 30 deletions

View File

@@ -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.",

View File

@@ -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) => {

View File

@@ -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;
}
});
};

View File

@@ -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<AppConnection, (server:
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter
[AppConnection.Redis]: registerRedisConnectionRouter,
[AppConnection.Chef]: registerChefConnectionRouter
};

View File

@@ -0,0 +1,13 @@
import { ChefSyncSchema, CreateChefSyncSchema, UpdateChefSyncSchema } from "@app/services/secret-sync/chef";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerChefSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Chef,
server,
responseSchema: ChefSyncSchema,
createSchema: CreateChefSyncSchema,
updateSchema: UpdateChefSyncSchema
});

View File

@@ -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<SecretSync, (server: Fastif
[SecretSync.Netlify]: registerNetlifySyncRouter,
[SecretSync.Northflank]: registerNorthflankSyncRouter,
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
[SecretSync.Chef]: registerChefSyncRouter
};

View File

@@ -24,6 +24,7 @@ import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/s
import { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/services/secret-sync/chef";
import {
CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema
@@ -88,7 +89,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
NetlifySyncSchema,
NorthflankSyncSchema,
BitbucketSyncSchema,
LaravelForgeSyncSchema
LaravelForgeSyncSchema,
ChefSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -123,7 +125,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
NetlifySyncListItemSchema,
NorthflankSyncListItemSchema,
BitbucketSyncListItemSchema,
LaravelForgeSyncListItemSchema
LaravelForgeSyncListItemSchema,
ChefSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -39,6 +39,7 @@ export enum AppConnection {
Okta = "okta",
Redis = "redis",
LaravelForge = "laravel-forge",
Chef = "chef",
Northflank = "northflank"
}

View File

@@ -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 (

View File

@@ -41,6 +41,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[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<AppConnection, AppConnectionPlanTyp
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular,
[AppConnection.Redis]: AppConnectionPlanType.Regular,
[AppConnection.Chef]: AppConnectionPlanType.Regular,
[AppConnection.Northflank]: AppConnectionPlanType.Regular
};

View File

@@ -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";
@@ -174,7 +176,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -881,6 +884,7 @@ export const appConnectionServiceFactory = ({
netlify: netlifyConnectionService(connectAppConnectionById),
northflank: northflankConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById),
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
chef: chefConnectionService(connectAppConnectionById)
};
};

View File

@@ -82,6 +82,12 @@ import {
TChecklyConnectionInput,
TValidateChecklyConnectionCredentialsSchema
} from "./checkly";
import {
TChefConnection,
TChefConnectionConfig,
TChefConnectionInput,
TValidateChefConnectionCredentialsSchema
} from "./chef";
import {
TCloudflareConnection,
TCloudflareConnectionConfig,
@@ -282,6 +288,7 @@ export type TAppConnection = { id: string } & (
| TNorthflankConnection
| TOktaConnection
| TRedisConnection
| TChefConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -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;

View File

@@ -0,0 +1,3 @@
export enum ChefConnectionMethod {
UserKey = "user-key"
}

View File

@@ -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<string, string> = {};
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<TChefDataBag[]> => {
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<Record<string, string>>(`${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<TChefDataBagItem[]> => {
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<Record<string, string>>(`${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<TChefDataBagItemContent> => {
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<TChefDataBagItemContent>(`${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<void> => {
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"
});
}
};

View File

@@ -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()
});

View File

@@ -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<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
};
};

View File

@@ -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<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"> & {
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;
};

View File

@@ -0,0 +1,4 @@
export * from "./chef-connection-enums";
export * from "./chef-connection-fns";
export * from "./chef-connection-schemas";
export * from "./chef-connection-types";

View File

@@ -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 () => {

View File

@@ -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
};

View File

@@ -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<TChefDataBagItemContent> => {
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<TChefSecrets> => {
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<string, TChefSecret>
) => {
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<TSecretMap> {
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);
}
};

View File

@@ -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)
});

View File

@@ -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<typeof ChefSyncListItemSchema>;
export type TChefSync = z.infer<typeof ChefSyncSchema>;
export type TChefSyncInput = z.infer<typeof CreateChefSyncSchema>;
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;
};

View File

@@ -0,0 +1,4 @@
export * from "./chef-sync-constants";
export * from "./chef-sync-fns";
export * from "./chef-sync-schemas";
export * from "./chef-sync-types";

View File

@@ -30,7 +30,8 @@ export enum SecretSync {
Netlify = "netlify",
Northflank = "northflank",
Bitbucket = "bitbucket",
LaravelForge = "laravel-forge"
LaravelForge = "laravel-forge",
Chef = "chef"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -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, TSecretSyncListItem> = {
[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}`

View File

@@ -34,7 +34,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[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<SecretSync, AppConnection> = {
@@ -69,7 +70,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[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<SecretSync, SecretSyncPlanType> = {
@@ -104,7 +106,8 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[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<SecretSync, string[]> = {
@@ -148,7 +151,8 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
[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<SecretSync, DestinationDupl
[SecretSync.Netlify]: defaultDuplicateCheck,
[SecretSync.Northflank]: defaultDuplicateCheck,
[SecretSync.Bitbucket]: defaultDuplicateCheck,
[SecretSync.LaravelForge]: defaultDuplicateCheck
[SecretSync.LaravelForge]: defaultDuplicateCheck,
[SecretSync.Chef]: defaultDuplicateCheck
};

View File

@@ -21,6 +21,7 @@ import {
TCamundaSyncListItem,
TCamundaSyncWithCredentials
} from "@app/services/secret-sync/camunda";
import { TChefSync, TChefSyncInput, TChefSyncListItem, TChefSyncWithCredentials } from "@app/services/secret-sync/chef";
import {
TDatabricksSync,
TDatabricksSyncInput,
@@ -169,6 +170,7 @@ export type TSecretSync =
| TGitHubSync
| TGcpSync
| TAzureKeyVaultSync
| TChefSync
| TAzureAppConfigurationSync
| TAzureDevOpsSync
| TDatabricksSync
@@ -202,6 +204,7 @@ export type TSecretSyncWithCredentials =
| TGitHubSyncWithCredentials
| TGcpSyncWithCredentials
| TAzureKeyVaultSyncWithCredentials
| TChefSyncWithCredentials
| TAzureAppConfigurationSyncWithCredentials
| TAzureDevOpsSyncWithCredentials
| TDatabricksSyncWithCredentials
@@ -236,6 +239,7 @@ export type TSecretSyncInput =
| TGitHubSyncInput
| TGcpSyncInput
| TAzureKeyVaultSyncInput
| TChefSyncInput
| TAzureAppConfigurationSyncInput
| TAzureDevOpsSyncInput
| TDatabricksSyncInput
@@ -270,6 +274,7 @@ export type TSecretSyncListItem =
| TGitHubSyncListItem
| TGcpSyncListItem
| TAzureKeyVaultSyncListItem
| TChefSyncListItem
| TAzureAppConfigurationSyncListItem
| TAzureDevOpsSyncListItem
| TDatabricksSyncListItem

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/chef/available"
---

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/chef"
---
<Note>
Check out the configuration docs for [Chef
Connections](/integrations/app-connections/chef) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/chef/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/chef/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/chef/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/chef"
---

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/chef/{connectionId}"
---
<Note>
Check out the configuration docs for [Chef
Connections](/integrations/app-connections/chef) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/chef"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/chef/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/chef/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/chef/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/import-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/chef"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/chef/{syncId}"
---

View File

@@ -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"
]
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -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.
<Accordion title="If you don't have a starter kit">
<Warning>
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.
</Warning>
<Steps>
<Step title="Navigate to your Chef Server Dashboard, and click on the 'Organizations' tab">
![Chef Server User Keys](/images/app-connections/chef/chef-dashboard.png)
</Step>
<Step title="Click on the organization you want to connect to, and then click on the 'Starter Kit' button">
![Starter Kit](/images/app-connections/chef/starter-kit.png)
</Step>
<Step title="Click on the 'Download Starter Kit' button to download the starter kit">
![Download Starter
Kit](/images/app-connections/chef/download-starter-kit.png)
</Step>
<Step title='Download the starter kit zip file and extract the contents'>
![Extract Starter
Kit](/images/app-connections/chef/extract-starter-kit.png)
</Step>
</Steps>
</Accordion>
<Steps>
<Step title="Open your starter kit's folder">
Open your starter kit's folder(or `chef-repo`) and navigate to the `.chef`
folder.
<Note>
Please make sure you have hidden files visible in your file explorer.
</Note>
![.chef folder](/images/app-connections/chef/chef-folder.png)
</Step>
<Step title='Copy the private key'>
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.
</Step>
<Step title='Open the config.rb 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.
</Step>
</Steps>
## Create a Chef Connection in Infisical via UI
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
In your Infisical dashboard, navigate to the **App Connections** page in the desired project.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Select Chef Connection">
Click **+ Add Connection** and choose **Chef** Connection from the list of integrations.
![Select Chef Connection](/images/app-connections/chef/app-connection-option.png)
</Step>
<Step title="Fill out the Chef Connection form">
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)
</Step>
<Step title="Connection created">
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)
</Step>
</Steps>
</Tab>
<Tab title="API">
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",
}
}
}
```
</Tab>
</Tabs>

View File

@@ -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)
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Add Sync">
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)
</Step>
<Step title="Select 'Chef'">
![Select Chef](/images/secret-syncs/chef/select-option.png)
</Step>
<Step title="Configure source">
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.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
</Step>
<Step title="Configure destination">
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.
</Step>
<Step title="Configure Sync Options">
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.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **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.
</Step>
<Step title="Configure details">
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.
</Step>
<Step title="Review configuration">
Review your Chef Sync configuration, then click **Create Sync**.
![Review Configuration](/images/secret-syncs/chef/sync-review.png)
</Step>
<Step title="Sync created">
If enabled, your Chef Sync will begin syncing your secrets to the destination endpoint.
![Sync Created](/images/secret-syncs/chef/sync-created.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
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"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -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());

View File

@@ -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());

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -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 (
<>
<SecretSyncConnectionField onChange={handleChangeConnection} />
<Controller
name="destinationConfig.dataBagName"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Data Bag">
<FilterableSelect
menuPlacement="top"
isLoading={isDataBagsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={dataBags?.find((dataBag) => dataBag.name === value) ?? null}
onChange={(option) => {
const selectedDataBag = option as SingleValue<TChefDataBag>;
onChange(selectedDataBag?.name ?? "");
setValue("destinationConfig.dataBagItemName", "");
}}
options={dataBags}
placeholder="Select a data bag..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.dataBagItemName"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Data Bag Item">
<FilterableSelect
menuPlacement="top"
isLoading={isDataBagItemsLoading && Boolean(connectionId && dataBagName)}
isDisabled={!connectionId || !dataBagName}
value={dataBagItems?.find((dataBagItem) => dataBagItem.name === value) ?? null}
onChange={(option) => {
const selectedDataBagItem = option as SingleValue<TChefDataBagItem>;
onChange(selectedDataBagItem?.name ?? "");
}}
options={dataBagItems}
placeholder="Select a data bag item..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
</>
);
};

View File

@@ -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 <BitbucketSyncFields />;
case SecretSync.LaravelForge:
return <LaravelForgeSyncFields />;
case SecretSync.Chef:
return <ChefSyncFields />;
case SecretSync.Northflank:
return <NorthflankSyncFields />;
default:

View File

@@ -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:

View File

@@ -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<TSecretSyncForm & { destination: SecretSync.Chef }>();
const dataBagName = watch("destinationConfig.dataBagName");
const dataBagItemName = watch("destinationConfig.dataBagItemName");
return (
<>
<GenericFieldLabel label="Data Bag">{dataBagName}</GenericFieldLabel>
<GenericFieldLabel label="Data Bag Item">{dataBagItemName}</GenericFieldLabel>
</>
);
};

View File

@@ -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 = <LaravelForgeSyncReviewFields />;
break;
case SecretSync.Chef:
DestinationFieldsComponent = <ChefSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

@@ -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")
})
})
);

View File

@@ -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;

View File

@@ -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:

View File

@@ -121,6 +121,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.LaravelForge]: {
name: "Laravel Forge",
image: "Laravel Forge.png"
},
[SecretSync.Chef]: {
name: "Chef",
image: "Chef.png"
}
};
@@ -156,7 +160,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[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<

View File

@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";

View File

@@ -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<typeof chefConnectionKeys.listDataBags>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: chefConnectionKeys.listDataBags(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TChefDataBag[]>(
`/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<typeof chefConnectionKeys.listDataBagItems>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: chefConnectionKeys.listDataBagItems(connectionId, dataBagName),
queryFn: async () => {
const params = { dataBagName };
const { data } = await apiRequest.get<TChefDataBagItem[]>(
`/api/v1/app-connections/chef/${connectionId}/data-bag-items`,
{ params }
);
return data;
},
enabled: Boolean(connectionId && dataBagName),
...options
});
};

View File

@@ -0,0 +1,7 @@
export type TChefDataBag = {
name: string;
};
export type TChefDataBagItem = {
name: string;
};

View File

@@ -39,5 +39,6 @@ export enum AppConnection {
Northflank = "northflank",
Okta = "okta",
Redis = "redis",
LaravelForge = "laravel-forge"
LaravelForge = "laravel-forge",
Chef = "chef"
}

View File

@@ -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;
};

View File

@@ -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;
};
};

View File

@@ -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<TAppConnection, "name" | "id" | "projectId">;

View File

@@ -30,7 +30,8 @@ export enum SecretSync {
Netlify = "netlify",
Northflank = "northflank",
Bitbucket = "bitbucket",
LaravelForge = "laravel-forge"
LaravelForge = "laravel-forge",
Chef = "chef"
}
export enum SecretSyncStatus {

View File

@@ -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;
};
};

View File

@@ -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[] };

View File

@@ -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 <RailwayConnectionForm onSubmit={onSubmit} />;
case AppConnection.Checkly:
return <ChecklyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Chef:
return <ChefConnectionForm onSubmit={onSubmit} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} />;
case AppConnection.DigitalOcean:
@@ -329,6 +332,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Checkly:
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Chef:
return <ChefConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.DigitalOcean:

View File

@@ -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<void>;
};
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<typeof formSchema>;
export const ChefConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Chef,
method: ChefConnectionMethod.UserKey
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="credentials.serverUrl"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Chef Server URL (optional)"
tooltipText="Will default to Chef Cloud if not specified."
>
<Input
placeholder="https://api.chef.io"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Chef].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(ChefConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.orgName"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Organization Short Name"
>
<Input
className="border border-mineshaft-500 bg-mineshaft-900"
placeholder="your-org"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
name="credentials.userName"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="User Name"
>
<Input
className="border border-mineshaft-500 bg-mineshaft-900"
placeholder="your-username"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
name="credentials.privateKey"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Private Key"
tooltipText="Your Chef user's private key (.pem file)"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Chef"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -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 <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -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 <BitbucketSyncDestinationCol secretSync={secretSync} />;
case SecretSync.LaravelForge:
return <LaravelForgeSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Chef:
return <ChefSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

View File

@@ -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}`);
}

View File

@@ -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 (
<>
<GenericFieldLabel label="Data Bag">{destinationConfig.dataBagName}</GenericFieldLabel>
<GenericFieldLabel label="Data Bag Item">
{destinationConfig.dataBagItemName}
</GenericFieldLabel>
</>
);
};

View File

@@ -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 = <LaravelForgeSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Chef:
DestinationComponents = <ChefSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}

View File

@@ -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: