import { isEmptyString } from '@sindresorhus/is'; import { PLATFORM_HOST_TYPES } from '../../constants/platforms.ts'; import { logger } from '../../logger/index.ts'; import type { HostRule } from '../../types/index.ts'; import { detectPlatform } from '../common.ts'; import type { ResolvedChildEnv } from '../exec/utils.ts'; import { find, findDetachedCredentials, getAll } from '../host-rules.ts'; import { regEx } from '../regex.ts'; import { createURLFromHostOrURL, isHttpUrl } from '../url.ts'; import { addGitConfigEnvironmentVariables } from './config.ts'; import type { AuthenticationRule } from './types.ts'; import { getTokenCredentials, parseGitUrl } from './url.ts'; const platformHostTypes = new Set(PLATFORM_HOST_TYPES); const githubApiUrls = new Set([ 'github.com', 'api.github.com', 'https://api.github.com', 'https://api.github.com/', ]); /** * Append Git authentication configuration to a resolved child environment. * @returns a new environment without modifying the input */ export function getGitAuthenticatedEnvironmentVariables( environmentVariables: Readonly, originalGitUrl: string, { token, username, password, hostType, matchHost }: HostRule, ): ResolvedChildEnv { if (!token && !(username && password)) { logger.warn( { host: matchHost }, `Could not create environment variable for host as neither token or username and password was set`, ); return { ...environmentVariables }; } let authenticationRules: AuthenticationRule[]; if (token) { authenticationRules = getAuthenticationRulesWithToken( originalGitUrl, hostType, token, ); } else { const encodedUsername = encodeURIComponent(username!); const encodedPassword = encodeURIComponent(password!); authenticationRules = getAuthenticationRules( originalGitUrl, hostType, `${encodedUsername}:${encodedPassword}`, ); } return addGitConfigEnvironmentVariables( environmentVariables, authenticationRules.map((rule) => ({ key: `url.${rule.url}.insteadOf`, value: rule.insteadOf, })), ); } function getAuthenticationRulesWithToken( url: string, hostType: string | undefined | null, authToken: string, ): AuthenticationRule[] { const type = hostType ?? detectPlatform(url); const platform = hostType && platformHostTypes.has(hostType) ? hostType : detectPlatform(url); return getAuthenticationRules( url, type, getTokenCredentials(platform, authToken), ); } /** * Generates the authentication rules for later git usage for the given host * @link https://coolaj86.com/articles/vanilla-devops-git-credentials-cheatsheet/ * @param gitUrl Git repository URL * @param hostType Git host type * @param token Authentication token or `username:password` string */ export function getAuthenticationRules( gitUrl: string, hostType: string | undefined | null, token: string, ): AuthenticationRule[] { const authenticationRules = []; const hasUser = token.split(':').length > 1; const insteadUrl = parseGitUrl(gitUrl); let sshPort = insteadUrl.port; if (hostType === 'bitbucket-server') { // For Bitbucket Server/Data Center, `source` must be `bitbucket-server` // to generate HTTP(s) URLs correctly. // https://github.com/IonicaBizau/git-url-parse/blob/28828546c148d58bbcff61409915a4e1e8f7eb11/lib/index.js#L304 insteadUrl.source = 'bitbucket-server'; // sshPort is string, wrong type! // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72361 // https://github.com/IonicaBizau/parse-path/blob/87f71f273da90f85f6845937a70b7c032eeae4e3/lib/index.js#L45 // v8 ignore next -- TODO: add test #40625 if (!sshPort || isEmptyString(sshPort)) { // By default, bitbucket-server SSH port is 7999. // For non-default port, the generated auth config will likely be incorrect. sshPort = '7999'; } } const url = { ...insteadUrl }; // git-url-parse would prefix `x-token-auth:` again on bitbucket.org if (hasUser && url.source === 'bitbucket.org') { url.source = 'bitbucket'; } const protocol = regEx(/^https?$/).test(url.protocol) ? url.protocol : 'https'; // ssh protocol with user if empty url.token = hasUser ? token : `ssh:${token}`; authenticationRules.push({ url: url.toString(protocol), // only edge case, need to stringify ourself because the exact syntax is not supported by the library // https://github.com/IonicaBizau/git-url-parse/blob/246c9119fb42c2ea1c280028fe77c53eb34c190c/lib/index.js#L246 insteadOf: `ssh://git@${insteadUrl.resource}${ sshPort ? `:${sshPort}` : '' }/${insteadUrl.full_name}${insteadUrl.git_suffix ? '.git' : ''}`, }); // alternative ssh protocol with user if empty url.token = hasUser ? token : `git:${token}`; authenticationRules.push({ url: url.toString(protocol), insteadOf: { ...insteadUrl, port: sshPort }.toString('ssh'), }); // https protocol with no user as default fallback url.token = token; authenticationRules.push({ url: url.toString(protocol), insteadOf: insteadUrl.toString(protocol), }); return authenticationRules; } /** * Appends Git authentication for the repository `url` from the credentials registered from URL credentials for it with `hostType`, a username without a password as a percent-encoded token; configured host rules come through `getAll()` instead. * @returns a new environment without modifying the input */ function getGitUrlEnvironmentVariables( environmentVariables: Readonly, url: string, hostType: string, ): ResolvedChildEnv { const credentials = findDetachedCredentials({ hostType, url }); if (!credentials?.username) { return { ...environmentVariables }; } const auth: HostRule = credentials.password ? credentials : { token: encodeURIComponent(credentials.username) }; return getGitAuthenticatedEnvironmentVariables(environmentVariables, url, { ...auth, hostType, }); } /** * Appends Git authentication from the host rules with a `matchHost` for the platform host types and `hostTypes`, and, when `url` is given, for that single repository from the credentials registered from URL credentials for it with each of `hostTypes`. * @returns a new environment without modifying the input */ export function getGitEnvironmentVariables( environmentVariables: Readonly, hostTypes: readonly string[] = [], url?: string, ): ResolvedChildEnv { let gitEnvironmentVariables: ResolvedChildEnv = { ...environmentVariables }; // hard-coded logic to use authentication for github.com based on the githubToken for api.github.com const gitHubHostRule = find({ hostType: 'github', url: 'https://api.github.com/', }); if (gitHubHostRule?.token) { gitEnvironmentVariables = getGitAuthenticatedEnvironmentVariables( gitEnvironmentVariables, 'https://github.com/', gitHubHostRule, ); } // Manager-scoped rules are opt-in so unrelated credentials are not exposed to Git. const gitAllowedHostTypes = new Set([ ...PLATFORM_HOST_TYPES, ...hostTypes, ]); // filter rules without `matchHost` and `token` or username and password and github api github rules const hostRules = getAll() .filter((r) => r.matchHost && (r.token ?? (r.username && r.password))) .filter((r) => !gitHubHostRule || !githubApiUrls.has(r.matchHost!)); // for each hostRule without hostType we add additional authentication variables to the environmentVariables // for each hostRule with hostType we add additional authentication variables to the environmentVariables for (const hostRule of hostRules) { if (!hostRule.hostType || gitAllowedHostTypes.has(hostRule.hostType)) { gitEnvironmentVariables = addAuthFromHostRule( hostRule, gitEnvironmentVariables, ); } } // `getAll()` leaves out the rules registered from URL credentials, which only apply to the repository they came from if (url) { for (const hostType of hostTypes) { gitEnvironmentVariables = getGitUrlEnvironmentVariables( gitEnvironmentVariables, url, hostType, ); } } return gitEnvironmentVariables; } function addAuthFromHostRule( hostRule: HostRule, environmentVariables: Readonly, ): ResolvedChildEnv { const httpUrl = createURLFromHostOrURL(hostRule.matchHost!)?.toString(); if (isHttpUrl(httpUrl)) { logger.trace(`Adding Git authentication for ${httpUrl} using token auth.`); return getGitAuthenticatedEnvironmentVariables( environmentVariables, httpUrl!, hostRule, ); } logger.debug( `Could not parse registryUrl ${hostRule.matchHost!} or not using http(s). Ignoring`, ); return { ...environmentVariables }; }