From 62b57aa27c890c4ebfe25822bc2df6b76ac0fcf4 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Mon, 17 Apr 2023 08:16:02 +0200 Subject: [PATCH] feat: disable setting COMPOSER_AUTH for gitlab (#20634) Co-authored-by: Rhys Arkins Co-authored-by: Michael Kriese --- docs/usage/configuration-options.md | 25 ++ lib/config/options/index.ts | 14 + .../manager/composer/artifacts.spec.ts | 323 ++++++++++++++++++ lib/modules/manager/composer/artifacts.ts | 50 ++- lib/modules/manager/composer/utils.spec.ts | 43 +-- lib/modules/manager/composer/utils.ts | 12 +- lib/types/host-rules.ts | 1 + 7 files changed, 428 insertions(+), 40 deletions(-) diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index a37bd906c2..d9ed849b8e 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1348,6 +1348,31 @@ Example: If enabled, this allows a single TCP connection to remain open for multiple HTTP(S) requests/responses. +### artifactAuth + +You may use this field whenever it is needed to only enable authentication for a specific set of managers. + +For example, using this option could be used whenever authentication using Git for private composer packages is already being handled through the use of SSH keys, which results in no need for also setting up authentication using tokens. + +```json +{ + "hostRules": [ + { + "hostType": "gitlab", + "matchHost": "gitlab.myorg.com", + "token": "abc123", + "artifactAuth": ["composer"] + } + ] +} +``` + +Supported artifactAuth and hostType combinations: + +| artifactAuth | hostTypes | +| ------------ | ------------------------------------------- | +| `composer` | `gitlab`, `packagist`, `github`, `git-tags` | + ### matchHost This can be a base URL (e.g. `https://api.github.com`) or a hostname like `github.com` or `api.github.com`. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 233ba616f2..4702833112 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -2302,6 +2302,20 @@ const options: RenovateOptions[] = [ env: false, experimental: true, }, + { + name: 'artifactAuth', + description: + 'A list of package managers to enable artifact auth. Only managers on the list are enabled. All are enabled if `null`', + experimental: true, + type: 'array', + subType: 'string', + stage: 'repository', + parent: 'hostRules', + allowedValues: ['composer'], + default: null, + cli: false, + env: false, + }, { name: 'cacheHardTtlMinutes', description: diff --git a/lib/modules/manager/composer/artifacts.spec.ts b/lib/modules/manager/composer/artifacts.spec.ts index d748617197..bd197b7233 100644 --- a/lib/modules/manager/composer/artifacts.spec.ts +++ b/lib/modules/manager/composer/artifacts.spec.ts @@ -292,6 +292,329 @@ describe('modules/manager/composer/artifacts', () => { ]); }); + it('does set github COMPOSER_AUTH for github when only hostType git-tags artifactAuth does not include composer', async () => { + hostRules.add({ + hostType: 'github', + matchHost: 'api.github.com', + token: 'ghs_token', + }); + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + artifactAuth: [], + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghs_token"}}', + }, + }, + }, + ]); + }); + + it('does set github COMPOSER_AUTH for git-tags when only hostType github artifactAuth does not include composer', async () => { + hostRules.add({ + hostType: 'github', + matchHost: 'api.github.com', + token: 'ghs_token', + artifactAuth: [], + }); + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}', + }, + }, + }, + ]); + }); + + it('does not set github COMPOSER_AUTH when artifactAuth does not include composer, for both hostType github & git-tags', async () => { + hostRules.add({ + hostType: 'github', + matchHost: 'api.github.com', + token: 'ghs_token', + artifactAuth: [], + }); + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + artifactAuth: [], + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + expect(execSnapshots[0].options?.env).not.toContainKey('COMPOSER_AUTH'); + }); + + it('does not set gitlab COMPOSER_AUTH when artifactAuth does not include composer', async () => { + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + }); + hostRules.add({ + hostType: 'gitlab', + matchHost: 'gitlab.com', + token: 'gitlab-token', + artifactAuth: [], + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + postUpdateOptions: ['composerGitlabToken'], + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}', + }, + }, + }, + ]); + }); + + it('does not set packagist COMPOSER_AUTH when artifactAuth does not include composer', async () => { + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + }); + hostRules.add({ + hostType: PackagistDatasource.id, + matchHost: 'packagist.renovatebot.com', + username: 'some-username', + password: 'some-password', + artifactAuth: [], + }); + hostRules.add({ + hostType: PackagistDatasource.id, + matchHost: 'https://artifactory.yyyyyyy.com/artifactory/api/composer/', + username: 'some-other-username', + password: 'some-other-password', + artifactAuth: [], + }); + hostRules.add({ + hostType: PackagistDatasource.id, + username: 'some-other-username', + password: 'some-other-password', + artifactAuth: [], + }); + hostRules.add({ + hostType: PackagistDatasource.id, + matchHost: 'https://packages-bearer.example.com/', + token: 'abcdef0123456789', + artifactAuth: [], + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + postUpdateOptions: ['composerGitlabToken'], + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}', + }, + }, + }, + ]); + }); + + it('does set gitlab COMPOSER_AUTH when artifactAuth does include composer', async () => { + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + }); + hostRules.add({ + hostType: 'gitlab', + matchHost: 'gitlab.com', + token: 'gitlab-token', + artifactAuth: ['composer'], + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + postUpdateOptions: ['composerGitlabToken'], + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: + '{"github-oauth":{"github.com":"ghp_token"},' + + '"gitlab-token":{"gitlab.com":"gitlab-token"},' + + '"gitlab-domains":["gitlab.com"]}', + }, + }, + }, + ]); + }); + + it('does set packagist COMPOSER_AUTH when artifactAuth does include composer', async () => { + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + }); + hostRules.add({ + hostType: PackagistDatasource.id, + matchHost: 'packagist.renovatebot.com', + username: 'some-username', + password: 'some-password', + artifactAuth: ['composer'], + }); + hostRules.add({ + hostType: PackagistDatasource.id, + matchHost: 'https://artifactory.yyyyyyy.com/artifactory/api/composer/', + username: 'some-other-username', + password: 'some-other-password', + artifactAuth: ['composer'], + }); + hostRules.add({ + hostType: PackagistDatasource.id, + username: 'some-other-username', + password: 'some-other-password', + artifactAuth: ['composer'], + }); + hostRules.add({ + hostType: PackagistDatasource.id, + matchHost: 'https://packages-bearer.example.com/', + token: 'abcdef0123456789', + artifactAuth: ['composer'], + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + postUpdateOptions: ['composerGitlabToken'], + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: + '{"github-oauth":{"github.com":"ghp_token"},' + + '"http-basic":{' + + '"packagist.renovatebot.com":{"username":"some-username","password":"some-password"},' + + '"artifactory.yyyyyyy.com":{"username":"some-other-username","password":"some-other-password"}' + + '},' + + '"bearer":{"packages-bearer.example.com":"abcdef0123456789"}}', + }, + }, + }, + ]); + }); + it('returns updated composer.lock', async () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(); diff --git a/lib/modules/manager/composer/artifacts.ts b/lib/modules/manager/composer/artifacts.ts index 0eae7fefde..cc6acbbf26 100644 --- a/lib/modules/manager/composer/artifacts.ts +++ b/lib/modules/manager/composer/artifacts.ts @@ -27,6 +27,7 @@ import { findGithubToken, getComposerArguments, getPhpConstraint, + isArtifactAuthEnabled, requireComposerDependencyInstallation, takePersonalAccessTokenIfPossible, } from './utils'; @@ -34,27 +35,36 @@ import { function getAuthJson(): string | null { const authJson: AuthJson = {}; - const githubToken = findGithubToken({ + const githubHostRule = hostRules.find({ hostType: 'github', url: 'https://api.github.com/', }); - const gitTagsGithubToken = findGithubToken({ + const gitTagsHostRule = hostRules.find({ hostType: GitTagsDatasource.id, url: 'https://github.com', }); const selectedGithubToken = takePersonalAccessTokenIfPossible( - githubToken, - gitTagsGithubToken + isArtifactAuthEnabled(githubHostRule) + ? findGithubToken(githubHostRule) + : undefined, + isArtifactAuthEnabled(gitTagsHostRule) + ? findGithubToken(gitTagsHostRule) + : undefined ); + if (selectedGithubToken) { authJson['github-oauth'] = { 'github.com': selectedGithubToken, }; } - hostRules.findAll({ hostType: 'gitlab' })?.forEach((gitlabHostRule) => { + for (const gitlabHostRule of hostRules.findAll({ hostType: 'gitlab' })) { + if (!isArtifactAuthEnabled(gitlabHostRule)) { + continue; + } + if (gitlabHostRule?.token) { const host = gitlabHostRule.resolvedHost ?? 'gitlab.com'; authJson['gitlab-token'] = authJson['gitlab-token'] ?? {}; @@ -65,20 +75,24 @@ function getAuthJson(): string | null { ...(authJson['gitlab-domains'] ?? []), ]; } - }); + } - hostRules - .findAll({ hostType: PackagistDatasource.id }) - ?.forEach((hostRule) => { - const { resolvedHost, username, password, token } = hostRule; - if (resolvedHost && username && password) { - authJson['http-basic'] = authJson['http-basic'] ?? {}; - authJson['http-basic'][resolvedHost] = { username, password }; - } else if (resolvedHost && token) { - authJson.bearer = authJson.bearer ?? {}; - authJson.bearer[resolvedHost] = token; - } - }); + for (const packagistHostRule of hostRules.findAll({ + hostType: PackagistDatasource.id, + })) { + if (!isArtifactAuthEnabled(packagistHostRule)) { + continue; + } + + const { resolvedHost, username, password, token } = packagistHostRule; + if (resolvedHost && username && password) { + authJson['http-basic'] = authJson['http-basic'] ?? {}; + authJson['http-basic'][resolvedHost] = { username, password }; + } else if (resolvedHost && token) { + authJson.bearer = authJson.bearer ?? {}; + authJson.bearer[resolvedHost] = token; + } + } return is.emptyObject(authJson) ? null : JSON.stringify(authJson); } diff --git a/lib/modules/manager/composer/utils.spec.ts b/lib/modules/manager/composer/utils.spec.ts index c591825be7..481df517e6 100644 --- a/lib/modules/manager/composer/utils.spec.ts +++ b/lib/modules/manager/composer/utils.spec.ts @@ -308,21 +308,26 @@ describe('modules/manager/composer/utils', () => { matchHost: 'github.com', token: TOKEN_STRING, }); - expect( - findGithubToken({ - hostType: GitTagsDatasource.id, - url: 'https://github.com', - }) - ).toEqual(TOKEN_STRING); + + const foundHostRule = hostRules.find({ + hostType: GitTagsDatasource.id, + url: 'https://github.com', + }); + + expect(findGithubToken(foundHostRule)).toEqual(TOKEN_STRING); }); - it('returns undefined when no hostRule match search', () => { - expect( - findGithubToken({ - hostType: GitTagsDatasource.id, - url: 'https://github.com', - }) - ).toBeUndefined(); + it('returns undefined when no token is defined', () => { + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + }); + + const foundHostRule = hostRules.find({ + hostType: GitTagsDatasource.id, + url: 'https://github.com', + }); + expect(findGithubToken(foundHostRule)).toBeUndefined(); }); it('remove x-access-token token prefix', () => { @@ -333,12 +338,12 @@ describe('modules/manager/composer/utils', () => { matchHost: 'github.com', token: TOKEN_STRING_WITH_PREFIX, }); - expect( - findGithubToken({ - hostType: GitTagsDatasource.id, - url: 'https://github.com', - }) - ).toEqual(TOKEN_STRING); + + const foundHostRule = hostRules.find({ + hostType: GitTagsDatasource.id, + url: 'https://github.com', + }); + expect(findGithubToken(foundHostRule)).toEqual(TOKEN_STRING); }); }); diff --git a/lib/modules/manager/composer/utils.ts b/lib/modules/manager/composer/utils.ts index 330f50bc71..8342798971 100644 --- a/lib/modules/manager/composer/utils.ts +++ b/lib/modules/manager/composer/utils.ts @@ -3,8 +3,8 @@ import { quote } from 'shlex'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; +import type { HostRuleSearchResult } from '../../../types'; import type { ToolConstraint } from '../../../util/exec/types'; -import { HostRuleSearch, find as findHostRule } from '../../../util/host-rules'; import { api, id as composerVersioningId } from '../../versioning/composer'; import type { UpdateArtifactsConfig } from '../types'; import type { ComposerConfig, ComposerLock } from './types'; @@ -111,8 +111,10 @@ export function extractConstraints( return res; } -export function findGithubToken(search: HostRuleSearch): string | undefined { - return findHostRule(search)?.token?.replace('x-access-token:', ''); +export function findGithubToken( + searchResult: HostRuleSearchResult +): string | undefined { + return searchResult?.token?.replace('x-access-token:', ''); } export function isGithubPersonalAccessToken(token: string): boolean { @@ -173,3 +175,7 @@ export function takePersonalAccessTokenIfPossible( return githubToken; } + +export function isArtifactAuthEnabled(rule: HostRuleSearchResult): boolean { + return !rule.artifactAuth || rule.artifactAuth.includes('composer'); +} diff --git a/lib/types/host-rules.ts b/lib/types/host-rules.ts index d5b23a80ac..ac02a813c3 100644 --- a/lib/types/host-rules.ts +++ b/lib/types/host-rules.ts @@ -14,6 +14,7 @@ export interface HostRuleSearchResult { dnsCache?: boolean; keepalive?: boolean; + artifactAuth?: string[] | null; } export interface HostRule extends HostRuleSearchResult {