Initial commit
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
// JFrog Artifactory credential validation.
|
||||
// Supports API keys (X-JFrog-Art-Api), Bearer tokens (JWT + reftkn), and Basic auth.
|
||||
|
||||
import { logUtil } from "../../utils/logger";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type JfrogCredentialType = "api-key" | "bearer" | "basic";
|
||||
|
||||
export interface JfrogCredential {
|
||||
type: JfrogCredentialType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface JfrogSession {
|
||||
baseUrl: string;
|
||||
credential: JfrogCredential;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
canWrite: boolean;
|
||||
npmRepos: string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Credential detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Auto-detect the credential type from its format.
|
||||
* - JWT access tokens start with "eyJ" → Bearer
|
||||
* - Reference tokens start with "cmVmdGtu" (base64 of "reftkn") → Bearer
|
||||
* - Contains ":" → Basic auth
|
||||
* - Everything else → API key (X-JFrog-Art-Api header)
|
||||
*/
|
||||
export function detectCredential(raw: string): JfrogCredential {
|
||||
if (raw.startsWith("eyJ") || raw.startsWith("cmVmdGtu")) {
|
||||
return { type: "bearer", value: raw };
|
||||
}
|
||||
if (raw.includes(":") || (raw.length < 60 && raw.includes("="))) {
|
||||
return { type: "basic", value: raw };
|
||||
}
|
||||
return { type: "api-key", value: raw };
|
||||
}
|
||||
|
||||
export function authHeader(cred: JfrogCredential): Record<string, string> {
|
||||
switch (cred.type) {
|
||||
case "api-key":
|
||||
return { "X-JFrog-Art-Api": cred.value };
|
||||
case "bearer":
|
||||
return { Authorization: `Bearer ${cred.value}` };
|
||||
case "basic":
|
||||
return { Authorization: `Basic ${cred.value}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
session: JfrogSession | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full credential validation pipeline:
|
||||
* 1. Ping (best-effort; reference tokens often lack system access)
|
||||
* 2. Resolve username
|
||||
* 3. Enumerate npm repos
|
||||
* 4. Test write access with a probe package
|
||||
*/
|
||||
export async function validateCredentials(
|
||||
baseUrl: string,
|
||||
cred: JfrogCredential,
|
||||
): Promise<ValidationResult> {
|
||||
const headers = authHeader(cred);
|
||||
|
||||
// Step 1 — Ping
|
||||
let pingOk = false;
|
||||
const pingUrl = `${baseUrl}/api/system/ping`;
|
||||
logUtil.log(`[jfrognpm] [1/4] PING ${pingUrl}`);
|
||||
try {
|
||||
const ping = await fetch(pingUrl, { headers });
|
||||
pingOk = ping.status === 200;
|
||||
logUtil.log(
|
||||
`[jfrognpm] → ${ping.status} ${pingOk ? "OK" : "(scoped token — expected)"}`,
|
||||
);
|
||||
} catch {
|
||||
logUtil.log(`[jfrognpm] → connection failed`);
|
||||
}
|
||||
|
||||
// Step 2 — Resolve username
|
||||
logUtil.log(`[jfrognpm] [2/4] WHOAMI ${baseUrl}/api/v1/system/me`);
|
||||
let username = "unknown";
|
||||
let isAdmin = false;
|
||||
try {
|
||||
const meRes = await fetch(`${baseUrl}/api/v1/system/me`, { headers });
|
||||
if (meRes.ok) {
|
||||
const me = (await meRes.json()) as any;
|
||||
username = me.username ?? me.name ?? "unknown";
|
||||
isAdmin = me.admin ?? false;
|
||||
logUtil.log(`[jfrognpm] → ${username} (admin=${isAdmin})`);
|
||||
} else {
|
||||
logUtil.log(`[jfrognpm] → ${meRes.status} — continuing`);
|
||||
}
|
||||
} catch {
|
||||
logUtil.log(`[jfrognpm] → failed — continuing`);
|
||||
}
|
||||
|
||||
// Step 3 — Enumerate npm repos
|
||||
logUtil.log(
|
||||
`[jfrognpm] [3/4] LIST ${baseUrl}/api/repositories?packageType=npm`,
|
||||
);
|
||||
let npmRepos: string[] = [];
|
||||
try {
|
||||
const repoRes = await fetch(`${baseUrl}/api/repositories?packageType=npm`, {
|
||||
headers,
|
||||
});
|
||||
if (repoRes.ok) {
|
||||
const repos = (await repoRes.json()) as any[];
|
||||
npmRepos = (repos ?? []).map((r: any) => r.key);
|
||||
logUtil.log(
|
||||
`[jfrognpm] → ${npmRepos.length} repo(s): ${npmRepos.join(", ") || "(none)"}`,
|
||||
);
|
||||
} else {
|
||||
logUtil.log(`[jfrognpm] → ${repoRes.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
logUtil.log(`[jfrognpm] → failed: ${e}`);
|
||||
}
|
||||
|
||||
if (!pingOk && npmRepos.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
session: null,
|
||||
error: `Token rejected — unable to ping or list npm repos`,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 4 — Test write access
|
||||
logUtil.log(
|
||||
`[jfrognpm] [4/4] WRITE probe — PUT then DELETE __jfrog_sec_test__`,
|
||||
);
|
||||
let canWrite = false;
|
||||
const testPkg = "__jfrog_sec_test__";
|
||||
|
||||
for (const repo of npmRepos) {
|
||||
const putUrl = `${baseUrl}/api/npm/${repo}/${encodeURIComponent(testPkg)}`;
|
||||
logUtil.log(`[jfrognpm] PUT ${putUrl}`);
|
||||
try {
|
||||
const testBody = JSON.stringify({
|
||||
name: testPkg,
|
||||
version: "1.0.0",
|
||||
_id: testPkg,
|
||||
"dist-tags": { latest: "1.0.0" },
|
||||
versions: {
|
||||
"1.0.0": {
|
||||
name: testPkg,
|
||||
version: "1.0.0",
|
||||
dist: {
|
||||
tarball: `${baseUrl}/api/npm/${repo}/${testPkg}/-/${testPkg}-1.0.0.tgz`,
|
||||
},
|
||||
},
|
||||
},
|
||||
_attachments: {
|
||||
[`${testPkg}-1.0.0.tgz`]: {
|
||||
content_type: "application/octet-stream",
|
||||
data: Buffer.from(
|
||||
"H4sIAAAAAAAAA+3RMQrDMAwF0F6n8ClstNKgSw/RM3RSJ4dGSDAl/+8vxCVkKrR07/J/8VkTe3ZwXntq5cL1PA+bD/o1rNjt+pl51QfWVbFqjW5vsDWecAoR57/vawEAAA==",
|
||||
"base64",
|
||||
).toString(),
|
||||
length: 44,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const putRes = await fetch(putUrl, {
|
||||
method: "PUT",
|
||||
headers: { ...headers, "Content-Type": "application/json" },
|
||||
body: testBody,
|
||||
});
|
||||
|
||||
if (putRes.ok || putRes.status === 409) {
|
||||
canWrite = true;
|
||||
logUtil.log(
|
||||
`[jfrognpm] → ${putRes.status} — WRITE CONFIRMED on ${repo}`,
|
||||
);
|
||||
} else {
|
||||
logUtil.log(`[jfrognpm] → ${putRes.status} — no write access`);
|
||||
}
|
||||
|
||||
logUtil.log(`[jfrognpm] DELETE ${putUrl}`);
|
||||
await fetch(putUrl, { method: "DELETE", headers }).catch(() => {});
|
||||
} catch (e) {
|
||||
logUtil.log(`[jfrognpm] → error: ${e}`);
|
||||
}
|
||||
|
||||
if (canWrite) break;
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
session: {
|
||||
baseUrl,
|
||||
credential: cred,
|
||||
username,
|
||||
isAdmin,
|
||||
canWrite,
|
||||
npmRepos,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user