mirror of
https://github.com/github/codeql-action.git
synced 2026-10-29 17:17:15 +00:00
Merge pull request #3563 from github/mbg/private-registry/oidc
Accept OIDC configurations in `start-proxy`
This commit is contained in:
@@ -12,6 +12,7 @@ import { scanArtifactsForTokens } from "./artifact-scanner";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import * as json from "./json";
|
||||
import { Language } from "./languages";
|
||||
import { Logger, withGroup } from "./logging";
|
||||
import {
|
||||
@@ -261,7 +262,7 @@ export function getArtifactSuffix(matrix: string | undefined): string {
|
||||
if (matrix) {
|
||||
try {
|
||||
const matrixObject = JSON.parse(matrix);
|
||||
if (matrixObject !== null && typeof matrixObject === "object") {
|
||||
if (json.isObject(matrixObject)) {
|
||||
for (const matrixKey of Object.keys(matrixObject as object).sort())
|
||||
suffix += `-${matrixObject[matrixKey]}`;
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "path";
|
||||
import Long from "long";
|
||||
|
||||
import { DocUrl } from "./doc-url";
|
||||
import * as json from "./json";
|
||||
import { Logger } from "./logging";
|
||||
import type * as sarif from "./sarif";
|
||||
|
||||
@@ -189,7 +190,7 @@ export function resolveUriToFile(
|
||||
typeof location.index !== "number" ||
|
||||
location.index < 0 ||
|
||||
location.index >= artifacts.length ||
|
||||
typeof artifacts[location.index].location !== "object"
|
||||
!json.isObject(artifacts[location.index].location)
|
||||
) {
|
||||
logger.debug(`Ignoring location as index "${location.index}" is invalid`);
|
||||
return undefined;
|
||||
|
||||
38
src/json/index.ts
Normal file
38
src/json/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Represents a value we have obtained from parsing JSON which we know is an object,
|
||||
* and expect to be of some type `T` which has not yet been validated.
|
||||
*/
|
||||
export type UnvalidatedObject<T> = { [P in keyof T]?: unknown };
|
||||
|
||||
/** Represents a value we have obtained from parsing JSON which we know is an array. */
|
||||
export type UnvalidatedArray = unknown[];
|
||||
|
||||
/**
|
||||
* Attempts to parse `data` as JSON. This function does not perform any validation and will therefore
|
||||
* return a value of an `unknown` type if successful. Throws if `data` is not valid JSON.
|
||||
*/
|
||||
export function parseString(data: string): unknown {
|
||||
return JSON.parse(data) as unknown;
|
||||
}
|
||||
|
||||
/** Asserts that `value` is an object, which is not yet validated, but expected to be of type `T`. */
|
||||
export function isObject<T>(value: unknown): value is UnvalidatedObject<T> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Asserts that `value` is an array, which is not yet validated. */
|
||||
export function isArray(value: unknown): value is UnvalidatedArray {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Asserts that `value` is a string. */
|
||||
export function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is either a string or undefined. */
|
||||
export function isStringOrUndefined(
|
||||
value: unknown,
|
||||
): value is string | undefined {
|
||||
return value === undefined || isString(value);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getWorkflowRunID,
|
||||
} from "../actions-util";
|
||||
import { type CodeQL } from "../codeql";
|
||||
import * as json from "../json";
|
||||
import { Logger } from "../logging";
|
||||
import {
|
||||
DiskUsage,
|
||||
@@ -149,8 +150,7 @@ export async function getOverlayStatus(
|
||||
const contents = await fs.promises.readFile(statusFile, "utf-8");
|
||||
const parsed: unknown = JSON.parse(contents);
|
||||
if (
|
||||
typeof parsed !== "object" ||
|
||||
parsed === null ||
|
||||
!json.isObject<OverlayStatus>(parsed) ||
|
||||
typeof parsed["attemptedToBuildOverlayBaseDatabase"] !== "boolean" ||
|
||||
typeof parsed["builtOverlayBaseDatabase"] !== "boolean"
|
||||
) {
|
||||
|
||||
@@ -124,125 +124,110 @@ const mixedCredentials = [
|
||||
{ type: "git_source", host: "github.com/github", token: "mno" },
|
||||
];
|
||||
|
||||
test.serial(
|
||||
"getCredentials prefers registriesCredentials over registrySecrets",
|
||||
async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify([
|
||||
{ type: "npm_registry", host: "npm.pkg.github.com", token: "abc" },
|
||||
]),
|
||||
).toString("base64");
|
||||
const registrySecrets = JSON.stringify([
|
||||
{ type: "npm_registry", host: "registry.npmjs.org", token: "def" },
|
||||
]);
|
||||
test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify([
|
||||
{ type: "npm_registry", host: "npm.pkg.github.com", token: "abc" },
|
||||
]),
|
||||
).toString("base64");
|
||||
const registrySecrets = JSON.stringify([
|
||||
{ type: "npm_registry", host: "registry.npmjs.org", token: "def" },
|
||||
]);
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
registrySecrets,
|
||||
registryCredentials,
|
||||
undefined,
|
||||
);
|
||||
t.is(credentials.length, 1);
|
||||
t.is(credentials[0].host, "npm.pkg.github.com");
|
||||
},
|
||||
);
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
registrySecrets,
|
||||
registryCredentials,
|
||||
undefined,
|
||||
);
|
||||
t.is(credentials.length, 1);
|
||||
t.is(credentials[0].host, "npm.pkg.github.com");
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCredentials throws an error when configurations are not an array",
|
||||
async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify({ type: "npm_registry", token: "abc" }),
|
||||
).toString("base64");
|
||||
test("getCredentials throws an error when configurations are not an array", async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify({ type: "npm_registry", token: "abc" }),
|
||||
).toString("base64");
|
||||
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
registryCredentials,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Expected credentials data to be an array of configurations, but it is not.",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("getCredentials throws error when credential is not an object", async (t) => {
|
||||
const testCredentials = [["foo"], [null]].map(toEncodedJSON);
|
||||
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
registryCredentials,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Expected credentials data to be an array of configurations, but it is not.",
|
||||
message: "Invalid credentials - must be an object",
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCredentials throws error when credential is not an object",
|
||||
async (t) => {
|
||||
const testCredentials = [["foo"], [null]].map(toEncodedJSON);
|
||||
test("getCredentials throws error when credential is missing type", async (t) => {
|
||||
const testCredentials = [[{ token: "abc", url: "https://localhost" }]].map(
|
||||
toEncodedJSON,
|
||||
);
|
||||
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must be an object",
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCredentials throws error when credential is missing type",
|
||||
async (t) => {
|
||||
const testCredentials = [[{ token: "abc", url: "https://localhost" }]].map(
|
||||
toEncodedJSON,
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must have a type",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must have a type",
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
test("getCredentials throws error when credential missing host and url", async (t) => {
|
||||
const testCredentials = [
|
||||
[{ type: "npm_registry", token: "abc" }],
|
||||
[{ type: "npm_registry", token: "abc", host: null }],
|
||||
[{ type: "npm_registry", token: "abc", url: null }],
|
||||
].map(toEncodedJSON);
|
||||
|
||||
test.serial(
|
||||
"getCredentials throws error when credential missing host and url",
|
||||
async (t) => {
|
||||
const testCredentials = [
|
||||
[{ type: "npm_registry", token: "abc" }],
|
||||
[{ type: "npm_registry", token: "abc", host: null }],
|
||||
[{ type: "npm_registry", token: "abc", url: null }],
|
||||
].map(toEncodedJSON);
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must specify host or url",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must specify host or url",
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("getCredentials filters by language when specified", async (t) => {
|
||||
test("getCredentials filters by language when specified", async (t) => {
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
@@ -253,145 +238,270 @@ test.serial("getCredentials filters by language when specified", async (t) => {
|
||||
t.is(credentials[0].type, "maven_repository");
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCredentials returns all for a language when specified",
|
||||
async (t) => {
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
toEncodedJSON(mixedCredentials),
|
||||
KnownLanguage.go,
|
||||
);
|
||||
t.is(credentials.length, 2);
|
||||
test("getCredentials returns all for a language when specified", async (t) => {
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
toEncodedJSON(mixedCredentials),
|
||||
KnownLanguage.go,
|
||||
);
|
||||
t.is(credentials.length, 2);
|
||||
|
||||
const credentialsTypes = credentials.map((c) => c.type);
|
||||
t.assert(credentialsTypes.includes("goproxy_server"));
|
||||
t.assert(credentialsTypes.includes("git_source"));
|
||||
},
|
||||
);
|
||||
const credentialsTypes = credentials.map((c) => c.type);
|
||||
t.assert(credentialsTypes.includes("goproxy_server"));
|
||||
t.assert(credentialsTypes.includes("git_source"));
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCredentials returns all credentials when no language specified",
|
||||
async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
test("getCredentials returns all credentials when no language specified", async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
undefined,
|
||||
);
|
||||
t.is(credentials.length, mixedCredentials.length);
|
||||
},
|
||||
);
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
undefined,
|
||||
);
|
||||
t.is(credentials.length, mixedCredentials.length);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCredentials throws an error when non-printable characters are used",
|
||||
async (t) => {
|
||||
const invalidCredentials = [
|
||||
{ type: "nuget_feed", host: "1nuget.pkg.github.com", token: "abc\u0000" }, // Non-printable character in token
|
||||
{ type: "nuget_feed", host: "2nuget.pkg.github.com\u0001" }, // Non-printable character in host
|
||||
test("getCredentials throws an error when non-printable characters are used", async (t) => {
|
||||
const invalidCredentials: startProxyExports.RawCredential[] = [
|
||||
{ type: "nuget_feed", host: "1nuget.pkg.github.com", token: "abc\u0000" }, // Non-printable character in token
|
||||
{ type: "nuget_feed", host: "2nuget.pkg.github.com\u0001" }, // Non-printable character in host
|
||||
{
|
||||
type: "nuget_feed",
|
||||
host: "3nuget.pkg.github.com",
|
||||
password: "ghi\u0002",
|
||||
}, // Non-printable character in password
|
||||
{
|
||||
type: "nuget_feed",
|
||||
host: "4nuget.pkg.github.com",
|
||||
token: "ghi\x00",
|
||||
}, // Non-printable character in token
|
||||
];
|
||||
|
||||
for (const invalidCredential of invalidCredentials) {
|
||||
const credentialsInput = toEncodedJSON([invalidCredential]);
|
||||
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
type: "nuget_feed",
|
||||
host: "3nuget.pkg.github.com",
|
||||
password: "ghi\u0002",
|
||||
}, // Non-printable character in password
|
||||
{
|
||||
type: "nuget_feed",
|
||||
host: "4nuget.pkg.github.com",
|
||||
password: "ghi\x00",
|
||||
}, // Non-printable character in password
|
||||
];
|
||||
|
||||
for (const invalidCredential of invalidCredentials) {
|
||||
const credentialsInput = Buffer.from(
|
||||
JSON.stringify([invalidCredential]),
|
||||
).toString("base64");
|
||||
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Invalid credentials - fields must contain only printable characters",
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCredentials logs a warning when a PAT is used without a username",
|
||||
async (t) => {
|
||||
const loggedMessages = [];
|
||||
const logger = getRecordingLogger(loggedMessages);
|
||||
const likelyWrongCredentials = toEncodedJSON([
|
||||
{
|
||||
type: "git_server",
|
||||
host: "https://github.com/",
|
||||
password: `ghp_${makeTestToken()}`,
|
||||
message:
|
||||
"Invalid credentials - fields must contain only printable characters",
|
||||
},
|
||||
]);
|
||||
|
||||
const results = startProxyExports.getCredentials(
|
||||
logger,
|
||||
undefined,
|
||||
likelyWrongCredentials,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// The configuration should be accepted, despite the likely problem.
|
||||
t.assert(results);
|
||||
t.is(results.length, 1);
|
||||
t.is(results[0].type, "git_server");
|
||||
t.is(results[0].host, "https://github.com/");
|
||||
const validAzureCredential: startProxyExports.AzureConfig = {
|
||||
tenant_id: "12345678-1234-1234-1234-123456789012",
|
||||
client_id: "abcdef01-2345-6789-abcd-ef0123456789",
|
||||
};
|
||||
|
||||
const validAwsCredential: startProxyExports.AWSConfig = {
|
||||
aws_region: "us-east-1",
|
||||
account_id: "123456789012",
|
||||
role_name: "MY_ROLE",
|
||||
domain: "MY_DOMAIN",
|
||||
domain_owner: "987654321098",
|
||||
audience: "custom-audience",
|
||||
};
|
||||
|
||||
const validJFrogCredential: startProxyExports.JFrogConfig = {
|
||||
jfrog_oidc_provider_name: "MY_PROVIDER",
|
||||
audience: "jfrog-audience",
|
||||
identity_mapping_name: "my-mapping",
|
||||
};
|
||||
|
||||
test("getCredentials throws an error when non-printable characters are used for Azure OIDC", (t) => {
|
||||
for (const key of Object.keys(validAzureCredential)) {
|
||||
const invalidAzureCredential = {
|
||||
...validAzureCredential,
|
||||
[key]: "123\x00",
|
||||
};
|
||||
const invalidCredential: startProxyExports.RawCredential = {
|
||||
type: "nuget_feed",
|
||||
host: `${key}.nuget.pkg.github.com`,
|
||||
...invalidAzureCredential,
|
||||
};
|
||||
const credentialsInput = toEncodedJSON([invalidCredential]);
|
||||
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Invalid credentials - fields must contain only printable characters",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCredentials throws an error when non-printable characters are used for AWS OIDC", (t) => {
|
||||
for (const key of Object.keys(validAwsCredential)) {
|
||||
const invalidAwsCredential = {
|
||||
...validAwsCredential,
|
||||
[key]: "123\x00",
|
||||
};
|
||||
const invalidCredential: startProxyExports.RawCredential = {
|
||||
type: "nuget_feed",
|
||||
host: `${key}.nuget.pkg.github.com`,
|
||||
...invalidAwsCredential,
|
||||
};
|
||||
const credentialsInput = toEncodedJSON([invalidCredential]);
|
||||
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Invalid credentials - fields must contain only printable characters",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCredentials throws an error when non-printable characters are used for JFrog OIDC", (t) => {
|
||||
for (const key of Object.keys(validJFrogCredential)) {
|
||||
const invalidJFrogCredential = {
|
||||
...validJFrogCredential,
|
||||
[key]: "123\x00",
|
||||
};
|
||||
const invalidCredential: startProxyExports.RawCredential = {
|
||||
type: "nuget_feed",
|
||||
host: `${key}.nuget.pkg.github.com`,
|
||||
...invalidJFrogCredential,
|
||||
};
|
||||
const credentialsInput = toEncodedJSON([invalidCredential]);
|
||||
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Invalid credentials - fields must contain only printable characters",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCredentials accepts OIDC configurations", (t) => {
|
||||
const oidcConfigurations = [
|
||||
{
|
||||
type: "nuget_feed",
|
||||
host: "azure.pkg.github.com",
|
||||
...validAzureCredential,
|
||||
},
|
||||
{
|
||||
type: "nuget_feed",
|
||||
host: "aws.pkg.github.com",
|
||||
...validAwsCredential,
|
||||
},
|
||||
{
|
||||
type: "nuget_feed",
|
||||
host: "jfrog.pkg.github.com",
|
||||
...validJFrogCredential,
|
||||
},
|
||||
];
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
toEncodedJSON(oidcConfigurations),
|
||||
KnownLanguage.csharp,
|
||||
);
|
||||
t.is(credentials.length, 3);
|
||||
|
||||
t.assert(credentials.every((c) => c.type === "nuget_feed"));
|
||||
t.assert(credentials.some((c) => startProxyExports.isAzureConfig(c)));
|
||||
t.assert(credentials.some((c) => startProxyExports.isAWSConfig(c)));
|
||||
t.assert(credentials.some((c) => startProxyExports.isJFrogConfig(c)));
|
||||
});
|
||||
|
||||
test("getCredentials logs a warning when a PAT is used without a username", async (t) => {
|
||||
const loggedMessages = [];
|
||||
const logger = getRecordingLogger(loggedMessages);
|
||||
const likelyWrongCredentials = toEncodedJSON([
|
||||
{
|
||||
type: "git_server",
|
||||
host: "https://github.com/",
|
||||
password: `ghp_${makeTestToken()}`,
|
||||
},
|
||||
]);
|
||||
|
||||
const results = startProxyExports.getCredentials(
|
||||
logger,
|
||||
undefined,
|
||||
likelyWrongCredentials,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// The configuration should be accepted, despite the likely problem.
|
||||
t.assert(results);
|
||||
t.is(results.length, 1);
|
||||
t.is(results[0].type, "git_server");
|
||||
t.is(results[0].host, "https://github.com/");
|
||||
|
||||
if (startProxyExports.isUsernamePassword(results[0])) {
|
||||
t.assert(results[0].password?.startsWith("ghp_"));
|
||||
} else {
|
||||
t.fail("Expected a `UsernamePassword`-based credential.");
|
||||
}
|
||||
|
||||
// A warning should have been logged.
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
"using a GitHub Personal Access Token (PAT), but no username was provided",
|
||||
]);
|
||||
},
|
||||
);
|
||||
// A warning should have been logged.
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
"using a GitHub Personal Access Token (PAT), but no username was provided",
|
||||
]);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCredentials returns all credentials for Actions when using LANGUAGE_TO_REGISTRY_TYPE",
|
||||
async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
test("getCredentials returns all credentials for Actions when using LANGUAGE_TO_REGISTRY_TYPE", async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
KnownLanguage.actions,
|
||||
false,
|
||||
);
|
||||
t.is(credentials.length, mixedCredentials.length);
|
||||
},
|
||||
);
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
KnownLanguage.actions,
|
||||
false,
|
||||
);
|
||||
t.is(credentials.length, mixedCredentials.length);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCredentials returns no credentials for Actions when using NEW_LANGUAGE_TO_REGISTRY_TYPE",
|
||||
async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
test("getCredentials returns no credentials for Actions when using NEW_LANGUAGE_TO_REGISTRY_TYPE", async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
KnownLanguage.actions,
|
||||
true,
|
||||
);
|
||||
t.deepEqual(credentials, []);
|
||||
},
|
||||
);
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
KnownLanguage.actions,
|
||||
true,
|
||||
);
|
||||
t.deepEqual(credentials, []);
|
||||
});
|
||||
|
||||
test.serial("parseLanguage", async (t) => {
|
||||
test("parseLanguage", async (t) => {
|
||||
// Exact matches
|
||||
t.deepEqual(parseLanguage("csharp"), KnownLanguage.csharp);
|
||||
t.deepEqual(parseLanguage("cpp"), KnownLanguage.cpp);
|
||||
@@ -524,40 +634,6 @@ test.serial("getDownloadUrl returns matching release asset", async (t) => {
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("credentialToStr - hides passwords", (t) => {
|
||||
const secret = "password123";
|
||||
const credential = {
|
||||
type: "maven_credential",
|
||||
password: secret,
|
||||
url: "https://localhost",
|
||||
};
|
||||
|
||||
const str = startProxyExports.credentialToStr(credential);
|
||||
|
||||
t.false(str.includes(secret));
|
||||
t.is(
|
||||
"Type: maven_credential; Host: undefined; Url: https://localhost Username: undefined; Password: true; Token: false",
|
||||
str,
|
||||
);
|
||||
});
|
||||
|
||||
test.serial("credentialToStr - hides tokens", (t) => {
|
||||
const secret = "password123";
|
||||
const credential = {
|
||||
type: "maven_credential",
|
||||
token: secret,
|
||||
url: "https://localhost",
|
||||
};
|
||||
|
||||
const str = startProxyExports.credentialToStr(credential);
|
||||
|
||||
t.false(str.includes(secret));
|
||||
t.is(
|
||||
"Type: maven_credential; Host: undefined; Url: https://localhost Username: undefined; Password: false; Token: true",
|
||||
str,
|
||||
);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getSafeErrorMessage - returns actual message for `StartProxyError`",
|
||||
(t) => {
|
||||
|
||||
@@ -17,13 +17,26 @@ import {
|
||||
Feature,
|
||||
FeatureEnablement,
|
||||
} from "./feature-flags";
|
||||
import * as json from "./json";
|
||||
import { KnownLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
Address,
|
||||
RawCredential,
|
||||
Registry,
|
||||
Credential,
|
||||
AuthConfig,
|
||||
isToken,
|
||||
isAzureConfig,
|
||||
Token,
|
||||
UsernamePassword,
|
||||
AzureConfig,
|
||||
isAWSConfig,
|
||||
AWSConfig,
|
||||
isJFrogConfig,
|
||||
JFrogConfig,
|
||||
isUsernamePassword,
|
||||
hasUsername,
|
||||
RawCredential,
|
||||
} from "./start-proxy/types";
|
||||
import {
|
||||
ActionName,
|
||||
@@ -255,13 +268,19 @@ const NEW_LANGUAGE_TO_REGISTRY_TYPE: Required<RegistryMapping> = {
|
||||
*
|
||||
* @throws A `ConfigurationError` if the `Registry` value contains neither a `url` or `host` field.
|
||||
*/
|
||||
function getRegistryAddress(registry: Partial<Registry>): Address {
|
||||
if (isDefined(registry.url)) {
|
||||
function getRegistryAddress(
|
||||
registry: json.UnvalidatedObject<Registry>,
|
||||
): Address {
|
||||
if (
|
||||
isDefined(registry.url) &&
|
||||
json.isString(registry.url) &&
|
||||
json.isStringOrUndefined(registry.host)
|
||||
) {
|
||||
return {
|
||||
url: registry.url,
|
||||
host: registry.host,
|
||||
};
|
||||
} else if (isDefined(registry.host)) {
|
||||
} else if (isDefined(registry.host) && json.isString(registry.host)) {
|
||||
return {
|
||||
url: undefined,
|
||||
host: registry.host,
|
||||
@@ -274,6 +293,75 @@ function getRegistryAddress(registry: Partial<Registry>): Address {
|
||||
}
|
||||
}
|
||||
|
||||
/** Extracts an `AuthConfig` value from `config`. */
|
||||
export function getAuthConfig(
|
||||
config: json.UnvalidatedObject<AuthConfig>,
|
||||
): AuthConfig {
|
||||
// Start by checking for the OIDC configurations, since they have required properties
|
||||
// which we can use to identify them.
|
||||
if (isAzureConfig(config)) {
|
||||
return {
|
||||
tenant_id: config.tenant_id,
|
||||
client_id: config.client_id,
|
||||
} satisfies AzureConfig;
|
||||
} else if (isAWSConfig(config)) {
|
||||
return {
|
||||
aws_region: config.aws_region,
|
||||
account_id: config.account_id,
|
||||
role_name: config.role_name,
|
||||
domain: config.domain,
|
||||
domain_owner: config.domain_owner,
|
||||
audience: config.audience,
|
||||
} satisfies AWSConfig;
|
||||
} else if (isJFrogConfig(config)) {
|
||||
return {
|
||||
jfrog_oidc_provider_name: config.jfrog_oidc_provider_name,
|
||||
identity_mapping_name: config.identity_mapping_name,
|
||||
audience: config.audience,
|
||||
} satisfies JFrogConfig;
|
||||
} else if (isToken(config)) {
|
||||
// There are three scenarios for non-OIDC authentication based on the registry type:
|
||||
//
|
||||
// 1. `username`+`token`
|
||||
// 2. A `token` that combines the username and actual token, separated by ':'.
|
||||
// 3. `username`+`password`
|
||||
//
|
||||
// In all three cases, all fields are optional. If the `token` field is present,
|
||||
// we accept the configuration as a `Token` typed configuration, with the `token`
|
||||
// value and an optional `username`. Otherwise, we accept the configuration
|
||||
// typed as `UsernamePassword` (in the `else` clause below) with optional
|
||||
// username and password. I.e. a private registry type that uses 1. or 2.,
|
||||
// but has no `token` configured, will get accepted as `UsernamePassword` here.
|
||||
|
||||
if (isDefined(config.token)) {
|
||||
// Mask token to reduce chance of accidental leakage in logs, if we have one.
|
||||
core.setSecret(config.token);
|
||||
}
|
||||
|
||||
return { username: config.username, token: config.token } satisfies Token;
|
||||
} else {
|
||||
let username: string | undefined = undefined;
|
||||
let password: string | undefined = undefined;
|
||||
|
||||
// Both "username" and "password" are optional. If we have reached this point, we need
|
||||
// to validate which of them are present and that they have the correct type if so.
|
||||
if ("password" in config && json.isString(config.password)) {
|
||||
// Mask password to reduce chance of accidental leakage in logs, if we have one.
|
||||
core.setSecret(config.password);
|
||||
password = config.password;
|
||||
}
|
||||
if ("username" in config && json.isString(config.username)) {
|
||||
username = config.username;
|
||||
}
|
||||
|
||||
// Return the `UsernamePassword` object. Both username and password may be undefined.
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
} satisfies UsernamePassword;
|
||||
}
|
||||
}
|
||||
|
||||
// getCredentials returns registry credentials from action inputs.
|
||||
// It prefers `registries_credentials` over `registry_secrets`.
|
||||
// If neither is set, it returns an empty array.
|
||||
@@ -304,9 +392,9 @@ export function getCredentials(
|
||||
}
|
||||
|
||||
// Parse and validate the credentials
|
||||
let parsed: RawCredential[];
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(credentialsStr) as RawCredential[];
|
||||
parsed = json.parseString(credentialsStr);
|
||||
} catch {
|
||||
// Don't log the error since it might contain sensitive information.
|
||||
logger.error("Failed to parse the credentials data.");
|
||||
@@ -314,7 +402,7 @@ export function getCredentials(
|
||||
}
|
||||
|
||||
// Check that the parsed data is indeed an array.
|
||||
if (!Array.isArray(parsed)) {
|
||||
if (!json.isArray(parsed)) {
|
||||
throw new ConfigurationError(
|
||||
"Expected credentials data to be an array of configurations, but it is not.",
|
||||
);
|
||||
@@ -322,23 +410,17 @@ export function getCredentials(
|
||||
|
||||
const out: Credential[] = [];
|
||||
for (const e of parsed) {
|
||||
if (e === null || typeof e !== "object") {
|
||||
if (e === null || !json.isObject<RawCredential>(e)) {
|
||||
throw new ConfigurationError("Invalid credentials - must be an object");
|
||||
}
|
||||
|
||||
// The configuration must have a type.
|
||||
if (!isDefined(e.type)) {
|
||||
if (!isDefined(e.type) || !json.isString(e.type)) {
|
||||
throw new ConfigurationError("Invalid credentials - must have a type");
|
||||
}
|
||||
|
||||
// Mask credentials to reduce chance of accidental leakage in logs.
|
||||
if (isDefined(e.password)) {
|
||||
core.setSecret(e.password);
|
||||
}
|
||||
if (isDefined(e.token)) {
|
||||
core.setSecret(e.token);
|
||||
}
|
||||
|
||||
const authConfig = getAuthConfig(e);
|
||||
const address = getRegistryAddress(e);
|
||||
|
||||
// Filter credentials based on language if specified. `type` is the registry type.
|
||||
@@ -354,24 +436,25 @@ export function getCredentials(
|
||||
return str ? /^[\x20-\x7E]*$/.test(str) : true;
|
||||
};
|
||||
|
||||
if (
|
||||
!isPrintable(e.type) ||
|
||||
!isPrintable(e.host) ||
|
||||
!isPrintable(e.url) ||
|
||||
!isPrintable(e.username) ||
|
||||
!isPrintable(e.password) ||
|
||||
!isPrintable(e.token)
|
||||
) {
|
||||
throw new ConfigurationError(
|
||||
"Invalid credentials - fields must contain only printable characters",
|
||||
);
|
||||
// Ensure that all string fields only contain printable characters.
|
||||
for (const key of Object.keys(e)) {
|
||||
const val = e[key];
|
||||
if (typeof val === "string" && !isPrintable(val)) {
|
||||
throw new ConfigurationError(
|
||||
"Invalid credentials - fields must contain only printable characters",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If the password or token looks like a GitHub PAT, warn if no username is configured.
|
||||
if (
|
||||
!isDefined(e.username) &&
|
||||
((isDefined(e.password) && isPAT(e.password)) ||
|
||||
(isDefined(e.token) && isPAT(e.token)))
|
||||
((!hasUsername(authConfig) || !isDefined(authConfig.username)) &&
|
||||
isUsernamePassword(authConfig) &&
|
||||
isDefined(authConfig.password) &&
|
||||
isPAT(authConfig.password)) ||
|
||||
(isToken(authConfig) &&
|
||||
isDefined(authConfig.token) &&
|
||||
isPAT(authConfig.token))
|
||||
) {
|
||||
logger.warning(
|
||||
`A ${e.type} private registry is configured for ${e.host || e.url} using a GitHub Personal Access Token (PAT), but no username was provided. ` +
|
||||
@@ -382,9 +465,7 @@ export function getCredentials(
|
||||
|
||||
out.push({
|
||||
type: e.type,
|
||||
username: e.username,
|
||||
password: e.password,
|
||||
token: e.token,
|
||||
...authConfig,
|
||||
...address,
|
||||
});
|
||||
}
|
||||
@@ -498,17 +579,6 @@ export async function getDownloadUrl(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty-prints a `Credential` value to a string, but hides the actual password or token values.
|
||||
*
|
||||
* @param c The credential to convert to a string.
|
||||
*/
|
||||
export function credentialToStr(c: Credential): string {
|
||||
return `Type: ${c.type}; Host: ${c.host}; Url: ${c.url} Username: ${
|
||||
c.username
|
||||
}; Password: ${c.password !== undefined}; Token: ${c.token !== undefined}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to download a file from `url` into the toolcache.
|
||||
*
|
||||
|
||||
136
src/start-proxy/types.test.ts
Normal file
136
src/start-proxy/types.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import test from "ava";
|
||||
|
||||
import { setupTests } from "../testing-utils";
|
||||
|
||||
import * as types from "./types";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const validAzureCredential: types.AzureConfig = {
|
||||
tenant_id: "12345678-1234-1234-1234-123456789012",
|
||||
client_id: "abcdef01-2345-6789-abcd-ef0123456789",
|
||||
};
|
||||
|
||||
const validAwsCredential: types.AWSConfig = {
|
||||
aws_region: "us-east-1",
|
||||
account_id: "123456789012",
|
||||
role_name: "MY_ROLE",
|
||||
domain: "MY_DOMAIN",
|
||||
domain_owner: "987654321098",
|
||||
audience: "custom-audience",
|
||||
};
|
||||
|
||||
const validJFrogCredential: types.JFrogConfig = {
|
||||
jfrog_oidc_provider_name: "MY_PROVIDER",
|
||||
audience: "jfrog-audience",
|
||||
identity_mapping_name: "my-mapping",
|
||||
};
|
||||
|
||||
test("credentialToStr - pretty-prints valid username+password configurations", (t) => {
|
||||
const secret = "password123";
|
||||
const credential: types.Credential = {
|
||||
type: "maven_credential",
|
||||
username: "user",
|
||||
password: secret,
|
||||
url: "https://localhost",
|
||||
};
|
||||
|
||||
const str = types.credentialToStr(credential);
|
||||
|
||||
t.false(str.includes(secret));
|
||||
t.is(
|
||||
"Type: maven_credential; Url: https://localhost; Username: user; Password: ***;",
|
||||
str,
|
||||
);
|
||||
});
|
||||
|
||||
test("credentialToStr - pretty-prints valid username+token configurations", (t) => {
|
||||
const secret = "password123";
|
||||
const credential: types.Credential = {
|
||||
type: "maven_credential",
|
||||
username: "user",
|
||||
token: secret,
|
||||
url: "https://localhost",
|
||||
};
|
||||
|
||||
const str = types.credentialToStr(credential);
|
||||
|
||||
t.false(str.includes(secret));
|
||||
t.is(
|
||||
"Type: maven_credential; Url: https://localhost; Username: user; Token: ***;",
|
||||
str,
|
||||
);
|
||||
});
|
||||
|
||||
test("credentialToStr - pretty-prints valid Azure OIDC configurations", (t) => {
|
||||
const credential: types.Credential = {
|
||||
type: "maven_credential",
|
||||
url: "https://localhost",
|
||||
...validAzureCredential,
|
||||
};
|
||||
|
||||
const str = types.credentialToStr(credential);
|
||||
|
||||
t.is(
|
||||
"Type: maven_credential; Url: https://localhost; Tenant: 12345678-1234-1234-1234-123456789012; Client: abcdef01-2345-6789-abcd-ef0123456789;",
|
||||
str,
|
||||
);
|
||||
});
|
||||
|
||||
test("credentialToStr - pretty-prints valid AWS OIDC configurations", (t) => {
|
||||
const credential: types.Credential = {
|
||||
type: "maven_credential",
|
||||
url: "https://localhost",
|
||||
...validAwsCredential,
|
||||
};
|
||||
|
||||
const str = types.credentialToStr(credential);
|
||||
|
||||
t.is(
|
||||
"Type: maven_credential; Url: https://localhost; AWS Region: us-east-1; AWS Account: 123456789012; AWS Role: MY_ROLE; AWS Domain: MY_DOMAIN; AWS Domain Owner: 987654321098; AWS Audience: custom-audience;",
|
||||
str,
|
||||
);
|
||||
});
|
||||
|
||||
test("credentialToStr - pretty-prints valid JFrog OIDC configurations", (t) => {
|
||||
const credential: types.Credential = {
|
||||
type: "maven_credential",
|
||||
url: "https://localhost",
|
||||
...validJFrogCredential,
|
||||
};
|
||||
|
||||
const str = types.credentialToStr(credential);
|
||||
|
||||
t.is(
|
||||
"Type: maven_credential; Url: https://localhost; JFrog Provider: MY_PROVIDER; JFrog Identity Mapping: my-mapping; JFrog Audience: jfrog-audience;",
|
||||
str,
|
||||
);
|
||||
});
|
||||
|
||||
test("credentialToStr - hides passwords", (t) => {
|
||||
const secret = "password123";
|
||||
const credential = {
|
||||
type: "maven_credential",
|
||||
password: secret,
|
||||
url: "https://localhost",
|
||||
};
|
||||
|
||||
const str = types.credentialToStr(credential);
|
||||
|
||||
t.false(str.includes(secret));
|
||||
t.is("Type: maven_credential; Url: https://localhost; Password: ***;", str);
|
||||
});
|
||||
|
||||
test("credentialToStr - hides tokens", (t) => {
|
||||
const secret = "password123";
|
||||
const credential = {
|
||||
type: "maven_credential",
|
||||
token: secret,
|
||||
url: "https://localhost",
|
||||
};
|
||||
|
||||
const str = types.credentialToStr(credential);
|
||||
|
||||
t.false(str.includes(secret));
|
||||
t.is("Type: maven_credential; Url: https://localhost; Token: ***;", str);
|
||||
});
|
||||
@@ -1,22 +1,211 @@
|
||||
import type { UnvalidatedObject } from "../json";
|
||||
import * as json from "../json";
|
||||
import { isDefined } from "../util";
|
||||
|
||||
/**
|
||||
* After parsing configurations from JSON, we don't know whether all the keys we expect are
|
||||
* present or not. This type is used to represent such values, which we expect to be
|
||||
* `Credential` values, but haven't validated yet.
|
||||
*/
|
||||
export type RawCredential = Partial<Credential>;
|
||||
export type RawCredential = UnvalidatedObject<Credential>;
|
||||
|
||||
/** Usernames may be present for both authentication with tokens or passwords. */
|
||||
export type Username = {
|
||||
/** The username needed to authenticate to the package registry, if any. */
|
||||
username?: string;
|
||||
};
|
||||
|
||||
/** Decides whether `config` has a username. */
|
||||
export function hasUsername(config: AuthConfig): config is Username {
|
||||
return "username" in config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fields expected for authentication based on a username and password.
|
||||
* Both username and password are optional.
|
||||
*/
|
||||
export type UsernamePassword = {
|
||||
/** The password needed to authenticate to the package registry, if any. */
|
||||
password?: string;
|
||||
} & Username;
|
||||
|
||||
/** Decides whether `config` is based on a username and password. */
|
||||
export function isUsernamePassword(
|
||||
config: AuthConfig,
|
||||
): config is UsernamePassword {
|
||||
return hasUsername(config) && "password" in config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fields expected for token-based authentication.
|
||||
* Both username and token are optional.
|
||||
*/
|
||||
export type Token = {
|
||||
/** The token needed to authenticate to the package registry, if any. */
|
||||
token?: string;
|
||||
} & Username;
|
||||
|
||||
/** Decides whether `config` is token-based. */
|
||||
export function isToken(
|
||||
config: UnvalidatedObject<AuthConfig>,
|
||||
): config is Token {
|
||||
// The "username" field is optional, but should be a string if present.
|
||||
if ("username" in config && !json.isStringOrUndefined(config.username)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The "token" field is required, and must be a string or undefined.
|
||||
return "token" in config && json.isStringOrUndefined(config.token);
|
||||
}
|
||||
|
||||
/** Configuration for Azure OIDC. */
|
||||
export type AzureConfig = { tenant_id: string; client_id: string };
|
||||
|
||||
/** Decides whether `config` is an Azure OIDC configuration. */
|
||||
export function isAzureConfig(
|
||||
config: UnvalidatedObject<AuthConfig>,
|
||||
): config is AzureConfig {
|
||||
return (
|
||||
"tenant_id" in config &&
|
||||
"client_id" in config &&
|
||||
isDefined(config.tenant_id) &&
|
||||
isDefined(config.client_id) &&
|
||||
json.isString(config.tenant_id) &&
|
||||
json.isString(config.client_id)
|
||||
);
|
||||
}
|
||||
|
||||
/** Configuration for AWS OIDC. */
|
||||
export type AWSConfig = {
|
||||
aws_region: string;
|
||||
account_id: string;
|
||||
role_name: string;
|
||||
domain: string;
|
||||
domain_owner: string;
|
||||
audience?: string;
|
||||
};
|
||||
|
||||
/** Decides whether `config` is an AWS OIDC configuration. */
|
||||
export function isAWSConfig(
|
||||
config: UnvalidatedObject<AuthConfig>,
|
||||
): config is AWSConfig {
|
||||
// All of these properties are required.
|
||||
const requiredProperties = [
|
||||
"aws_region",
|
||||
"account_id",
|
||||
"role_name",
|
||||
"domain",
|
||||
"domain_owner",
|
||||
];
|
||||
|
||||
for (const property of requiredProperties) {
|
||||
if (
|
||||
!(property in config) ||
|
||||
!isDefined(config[property]) ||
|
||||
!json.isString(config[property])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The "audience" field is optional, but should be a string if present.
|
||||
if ("audience" in config && !json.isStringOrUndefined(config.audience)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Configuration for JFrog OIDC. */
|
||||
export type JFrogConfig = {
|
||||
jfrog_oidc_provider_name: string;
|
||||
audience?: string;
|
||||
identity_mapping_name?: string;
|
||||
};
|
||||
|
||||
/** Decides whether `config` is a JFrog OIDC configuration. */
|
||||
export function isJFrogConfig(
|
||||
config: UnvalidatedObject<AuthConfig>,
|
||||
): config is JFrogConfig {
|
||||
// The "audience" and "identity_mapping_name" fields are optional, but should be strings if present.
|
||||
if ("audience" in config && !json.isStringOrUndefined(config.audience)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
"identity_mapping_name" in config &&
|
||||
!json.isStringOrUndefined(config.identity_mapping_name)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
"jfrog_oidc_provider_name" in config &&
|
||||
isDefined(config.jfrog_oidc_provider_name) &&
|
||||
json.isString(config.jfrog_oidc_provider_name)
|
||||
);
|
||||
}
|
||||
|
||||
/** Represents all supported OIDC configurations. */
|
||||
export type OIDC = AzureConfig | AWSConfig | JFrogConfig;
|
||||
|
||||
/** All authentication-related fields. */
|
||||
export type AuthConfig = UsernamePassword | Token | OIDC;
|
||||
|
||||
/**
|
||||
* A package registry configuration includes identifying information as well as
|
||||
* authentication credentials.
|
||||
*/
|
||||
export type Credential = {
|
||||
/** The username needed to authenticate to the package registry, if any. */
|
||||
username?: string;
|
||||
/** The password needed to authenticate to the package registry, if any. */
|
||||
password?: string;
|
||||
/** The token needed to authenticate to the package registry, if any. */
|
||||
token?: string;
|
||||
} & Registry;
|
||||
export type Credential = AuthConfig & Registry;
|
||||
|
||||
/**
|
||||
* Pretty-prints a `Credential` value to a string, but hides the actual password or token values.
|
||||
*
|
||||
* @param credential The credential to convert to a string.
|
||||
*/
|
||||
export function credentialToStr(credential: Credential): string {
|
||||
let result: string = `Type: ${credential.type};`;
|
||||
|
||||
const appendIfDefined = (name: string, val: string | undefined) => {
|
||||
if (isDefined(val)) {
|
||||
result += ` ${name}: ${val};`;
|
||||
}
|
||||
};
|
||||
|
||||
appendIfDefined("Url", credential.url);
|
||||
appendIfDefined("Host", credential.host);
|
||||
|
||||
if (hasUsername(credential)) {
|
||||
appendIfDefined("Username", credential.username);
|
||||
}
|
||||
|
||||
if ("password" in credential) {
|
||||
appendIfDefined(
|
||||
"Password",
|
||||
isDefined(credential.password) ? "***" : undefined,
|
||||
);
|
||||
}
|
||||
if (isToken(credential)) {
|
||||
appendIfDefined("Token", isDefined(credential.token) ? "***" : undefined);
|
||||
}
|
||||
|
||||
if (isAzureConfig(credential)) {
|
||||
appendIfDefined("Tenant", credential.tenant_id);
|
||||
appendIfDefined("Client", credential.client_id);
|
||||
} else if (isAWSConfig(credential)) {
|
||||
appendIfDefined("AWS Region", credential.aws_region);
|
||||
appendIfDefined("AWS Account", credential.account_id);
|
||||
appendIfDefined("AWS Role", credential.role_name);
|
||||
appendIfDefined("AWS Domain", credential.domain);
|
||||
appendIfDefined("AWS Domain Owner", credential.domain_owner);
|
||||
appendIfDefined("AWS Audience", credential.audience);
|
||||
} else if (isJFrogConfig(credential)) {
|
||||
appendIfDefined("JFrog Provider", credential.jfrog_oidc_provider_name);
|
||||
appendIfDefined("JFrog Identity Mapping", credential.identity_mapping_name);
|
||||
appendIfDefined("JFrog Audience", credential.audience);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A package registry is identified by its type and address. */
|
||||
export type Registry = {
|
||||
|
||||
11
src/util.ts
11
src/util.ts
@@ -14,6 +14,7 @@ import type { CodeQL, VersionInfo } from "./codeql";
|
||||
import type { Pack } from "./config/db-config";
|
||||
import type { Config } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import * as json from "./json";
|
||||
import { Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
|
||||
@@ -603,19 +604,15 @@ export class HTTPError extends Error {
|
||||
export class ConfigurationError extends Error {}
|
||||
|
||||
export function asHTTPError(arg: any): HTTPError | undefined {
|
||||
if (
|
||||
typeof arg !== "object" ||
|
||||
arg === null ||
|
||||
typeof arg.message !== "string"
|
||||
) {
|
||||
if (!json.isObject<any>(arg) || !json.isString(arg.message)) {
|
||||
return undefined;
|
||||
}
|
||||
if (Number.isInteger(arg.status)) {
|
||||
return new HTTPError(arg.message as string, arg.status as number);
|
||||
return new HTTPError(arg.message, arg.status as number);
|
||||
}
|
||||
// See https://github.com/actions/toolkit/blob/acb230b99a46ed33a3f04a758cd68b47b9a82908/packages/tool-cache/src/tool-cache.ts#L19
|
||||
if (Number.isInteger(arg.httpStatusCode)) {
|
||||
return new HTTPError(arg.message as string, arg.httpStatusCode as number);
|
||||
return new HTTPError(arg.message, arg.httpStatusCode as number);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user