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 a37bd906c287fd..d9ed849b8e73de 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 233ba616f235a9..4702833112fe32 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 d7486171970396..bd197b7233d21c 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 0eae7fefdefad1..cc6acbbf266ff0 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 c591825be75663..481df517e6f260 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 330f50bc713892..8342798971adb7 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 d5b23a80ac246b..ac02a813c38a0b 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 {