fix: app-connection

This commit is contained in:
Piyush Gupta
2025-10-30 02:44:28 +05:30
parent 968284956b
commit ead15e459a
@@ -43,94 +43,75 @@ const formatPrivateKey = (key: string): string => {
return formattedKey; return formattedKey;
}; };
// Chef API authentication helper
const getChefAuthHeaders = ( const getChefAuthHeaders = (
method: string, method: string,
path: string, path: string,
body: string, body: string,
userId: string, userId: string,
privateKey: string, privateKey: string,
timestamp: string apiVersion: "1.0" | "1.3" = "1.3"
) => { ) => {
const hashedPath = crypto.createHash("sha256").update(path).digest("base64"); const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); // Remove milliseconds from timestamp
const hashedBody = crypto.createHash("sha256").update(body).digest("base64");
console.log("1", { // Calculate content hash based on version
method, let contentHash: string;
path, if (apiVersion === "1.3") {
body, contentHash = crypto.createHash("sha256").update(body).digest("base64");
userId, } else {
privateKey, contentHash = crypto.createHash("sha1").update(body).digest("base64");
timestamp }
});
const canonicalRequest = [ // Build canonical request based on version
`Method:${method}`, let canonicalRequest: string;
`Hashed Path:${hashedPath}`, if (apiVersion === "1.3") {
`X-Ops-Content-Hash:${hashedBody}`, canonicalRequest = [
`X-Ops-Timestamp:${timestamp}`, `Method:${method}`,
`X-Ops-UserId:${userId}` `Path:${path}`,
].join("\n"); `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 // Format the private key properly
const formattedKey = formatPrivateKey(privateKey); const formattedKey = formatPrivateKey(privateKey);
console.log("Chef Auth Debug - Key format:", { console.log("formattedKey", formattedKey);
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 // Sign the canonical request
let keyObject; const sign = crypto.createSign(apiVersion === "1.3" ? "RSA-SHA256" : "RSA-SHA1");
try { sign.update(canonicalRequest);
keyObject = crypto.createPrivateKey({ const signature = sign.sign(formattedKey, "base64");
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);
// Split signature into 60-character chunks
const authHeaders: Record<string, string> = {};
const signatureLines = signature.match(/.{1,60}/g) || [];
signatureLines.forEach((line, index) => { signatureLines.forEach((line, index) => {
headers[`X-Ops-Authorization-${index + 1}`] = line; authHeaders[`X-Ops-Authorization-${index + 1}`] = line;
}); });
console.log("7", headers); return {
Accept: "application/json",
return headers; "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 = () => { export const getChefConnectionListItem = () => {
@@ -148,18 +129,14 @@ export const listChefDataBags = async (appConnection: TChefConnection): Promise<
try { try {
const path = "/data"; const path = "/data";
const timestamp = `${new Date().toISOString().slice(0, -5)}Z`;
const body = ""; const body = "";
const hostServerUrl = await getChefServerUrl(serverUrl); const hostServerUrl = await getChefServerUrl(serverUrl);
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey, timestamp); const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, { const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
headers: { headers
...headers,
Accept: "application/json"
}
}); });
// Chef returns data bags as an object with keys being data bag names // Chef returns data bags as an object with keys being data bag names
@@ -184,34 +161,18 @@ export const validateChefConnectionCredentials = async (config: TChefConnectionC
try { try {
const path = `/organizations/${inputCredentials.orgName}/users/${inputCredentials.userName}`; 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); const hostServerUrl = await getChefServerUrl(inputCredentials.serverUrl);
console.log("Chef validate - attempting connection to:", hostServerUrl); const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey);
const headers = getChefAuthHeaders(
"GET",
path,
body,
inputCredentials.userName,
inputCredentials.privateKey,
timestamp
);
const data = await request.get(`${hostServerUrl}${path}`, { await request.get(`${hostServerUrl}${path}`, {
headers: { headers
...headers,
Accept: "application/json"
}
}); });
console.log("Chef validate - data:", data);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
const errorData = error.response?.data as { error?: string } | undefined;
console.log("Chef validate - error:", errorData);
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to validate Chef credentials: ${errorData?.error || error.message || "Unknown error"}` message: `Failed to validate Chef credentials: ${error.message || "Unknown error"}`
}); });
} }
throw new BadRequestError({ throw new BadRequestError({
@@ -232,18 +193,14 @@ export const listChefDataBagItems = async (
try { try {
const path = `/data/${dataBagName}`; const path = `/data/${dataBagName}`;
const timestamp = `${new Date().toISOString().slice(0, -5)}Z`;
const body = ""; const body = "";
const hostServerUrl = await getChefServerUrl(serverUrl); const hostServerUrl = await getChefServerUrl(serverUrl);
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey, timestamp); const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, { const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
headers: { headers
...headers,
Accept: "application/json"
}
}); });
// Chef returns data bag items as an object with keys being item names // Chef returns data bag items as an object with keys being item names
@@ -253,9 +210,9 @@ export const listChefDataBagItems = async (
})); }));
} catch (error) { } catch (error) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
// throw new BadRequestError({ throw new BadRequestError({
// message: `Failed to list Chef data bag items: ${error.response?.data?.error || error.message}` message: `Failed to list Chef data bag items: ${error.message || "Unknown error"}`
// }); });
} }
throw new BadRequestError({ throw new BadRequestError({
message: "Unable to list Chef data bag items" message: "Unable to list Chef data bag items"