Skip to content
30 changes: 30 additions & 0 deletions .vscode/tests.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
// Place your codeql-action workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Test Macro": {
"scope": "javascript, typescript",
"prefix": "testMacro",
"body": [
"const ${1:nameMacro} = test.macro({",
" exec: async (t: ExecutionContext<unknown>) => {},",
"",
" title: (providedTitle = \"\") => `${2:common title} - \\${providedTitle}`,",
"});",
],
"description": "An Ava test macro",
},
}
5 changes: 4 additions & 1 deletion lib/start-proxy-action.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 142 additions & 28 deletions src/start-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import * as startProxyExports from "./start-proxy";
import { parseLanguage } from "./start-proxy";
import * as statusReport from "./status-report";
import {
assertNotLogged,
checkExpectedLogMessages,
createFeatures,
getRecordingLogger,
makeTestToken,
RecordingLogger,
setupTests,
Expand Down Expand Up @@ -439,41 +439,155 @@ test("getCredentials accepts OIDC configurations", (t) => {
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([
const getCredentialsMacro = test.macro({
exec: async (
t: ExecutionContext<unknown>,
credentials: startProxyExports.RawCredential[],
checkAccepted: (
t: ExecutionContext<unknown>,
logger: RecordingLogger,
results: startProxyExports.Credential[],
) => void,
) => {
const logger = new RecordingLogger();
const credentialsString = toEncodedJSON(credentials);

const results = startProxyExports.getCredentials(
logger,
undefined,
credentialsString,
undefined,
);

checkAccepted(t, logger, results);
},

title: (providedTitle = "") => `getCredentials - ${providedTitle}`,
});

test(
"warns for PAT-like password without a username",
getCredentialsMacro,
[
{
type: "git_server",
host: "https://github.com/",
password: `ghp_${makeTestToken()}`,
},
]);

const results = startProxyExports.getCredentials(
logger,
undefined,
likelyWrongCredentials,
undefined,
);
],
(t, logger, results) => {
// The configurations 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, logger.messages, [
"using a GitHub Personal Access Token (PAT), but no username was provided",
]);
},
);

// 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/");
test(
"no warning for PAT-like password with a username",
getCredentialsMacro,
[
{
type: "git_server",
host: "https://github.com/",
username: "someone",
password: `ghp_${makeTestToken()}`,
},
],
(t, logger, results) => {
// The configurations 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.");
}

assertNotLogged(
t,
logger,
"using a GitHub Personal Access Token (PAT), but no username was provided",
);
},
);

if (startProxyExports.isUsernamePassword(results[0])) {
t.assert(results[0].password?.startsWith("ghp_"));
} else {
t.fail("Expected a `UsernamePassword`-based credential.");
}
test(
"warns for PAT-like token without a username",
getCredentialsMacro,
[
{
type: "git_server",
host: "https://github.com/",
token: `ghp_${makeTestToken()}`,
},
],
(t, logger, results) => {
// The configurations 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.isToken(results[0])) {
t.assert(results[0].token?.startsWith("ghp_"));
} else {
t.fail("Expected a `Token`-based credential.");
}

// A warning should have been logged.
checkExpectedLogMessages(t, logger.messages, [
"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(
"no warning for PAT-like token with a username",
getCredentialsMacro,
[
{
type: "git_server",
host: "https://github.com/",
username: "someone",
token: `ghp_${makeTestToken()}`,
},
],
(t, logger, results) => {
// The configurations 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.isToken(results[0])) {
t.assert(results[0].token?.startsWith("ghp_"));
} else {
t.fail("Expected a `Token`-based credential.");
}

assertNotLogged(
t,
logger,
"using a GitHub Personal Access Token (PAT), but no username was provided",
);
},
);

test("getCredentials returns all credentials for Actions when using LANGUAGE_TO_REGISTRY_TYPE", async (t) => {
const credentialsInput = toEncodedJSON(mixedCredentials);
Expand Down
21 changes: 12 additions & 9 deletions src/start-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,15 +447,18 @@ export function getCredentials(
}

// If the password or token looks like a GitHub PAT, warn if no username is configured.
if (
((!hasUsername(authConfig) || !isDefined(authConfig.username)) &&
isUsernamePassword(authConfig) &&
isDefined(authConfig.password) &&
isPAT(authConfig.password)) ||
(isToken(authConfig) &&
isDefined(authConfig.token) &&
isPAT(authConfig.token))
) {
const noUsername =
!hasUsername(authConfig) || !isDefined(authConfig.username);
const passwordIsPAT =
isUsernamePassword(authConfig) &&
isDefined(authConfig.password) &&
isPAT(authConfig.password);
const tokenIsPAT =
isToken(authConfig) &&
isDefined(authConfig.token) &&
isPAT(authConfig.token);

if (noUsername && (passwordIsPAT || tokenIsPAT)) {
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. ` +
`This may not work correctly. When configuring a private registry using a PAT, select "Username and password" and enter the username of the user ` +
Expand Down
Loading
Loading