feat: disable setting COMPOSER_AUTH for gitlab (#20634)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
DjordyKoert 2023-04-17 08:16:02 +02:00 committed by GitHub
parent 94b42c5123
commit 62b57aa27c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 428 additions and 40 deletions

View file

@ -1348,6 +1348,31 @@ Example:
If enabled, this allows a single TCP connection to remain open for multiple HTTP(S) requests/responses. 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 ### matchHost
This can be a base URL (e.g. `https://api.github.com`) or a hostname like `github.com` or `api.github.com`. This can be a base URL (e.g. `https://api.github.com`) or a hostname like `github.com` or `api.github.com`.

View file

@ -2302,6 +2302,20 @@ const options: RenovateOptions[] = [
env: false, env: false,
experimental: true, 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', name: 'cacheHardTtlMinutes',
description: description:

View file

@ -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 () => { it('returns updated composer.lock', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}'); fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll(); const execSnapshots = mockExecAll();

View file

@ -27,6 +27,7 @@ import {
findGithubToken, findGithubToken,
getComposerArguments, getComposerArguments,
getPhpConstraint, getPhpConstraint,
isArtifactAuthEnabled,
requireComposerDependencyInstallation, requireComposerDependencyInstallation,
takePersonalAccessTokenIfPossible, takePersonalAccessTokenIfPossible,
} from './utils'; } from './utils';
@ -34,27 +35,36 @@ import {
function getAuthJson(): string | null { function getAuthJson(): string | null {
const authJson: AuthJson = {}; const authJson: AuthJson = {};
const githubToken = findGithubToken({ const githubHostRule = hostRules.find({
hostType: 'github', hostType: 'github',
url: 'https://api.github.com/', url: 'https://api.github.com/',
}); });
const gitTagsGithubToken = findGithubToken({ const gitTagsHostRule = hostRules.find({
hostType: GitTagsDatasource.id, hostType: GitTagsDatasource.id,
url: 'https://github.com', url: 'https://github.com',
}); });
const selectedGithubToken = takePersonalAccessTokenIfPossible( const selectedGithubToken = takePersonalAccessTokenIfPossible(
githubToken, isArtifactAuthEnabled(githubHostRule)
gitTagsGithubToken ? findGithubToken(githubHostRule)
: undefined,
isArtifactAuthEnabled(gitTagsHostRule)
? findGithubToken(gitTagsHostRule)
: undefined
); );
if (selectedGithubToken) { if (selectedGithubToken) {
authJson['github-oauth'] = { authJson['github-oauth'] = {
'github.com': selectedGithubToken, 'github.com': selectedGithubToken,
}; };
} }
hostRules.findAll({ hostType: 'gitlab' })?.forEach((gitlabHostRule) => { for (const gitlabHostRule of hostRules.findAll({ hostType: 'gitlab' })) {
if (!isArtifactAuthEnabled(gitlabHostRule)) {
continue;
}
if (gitlabHostRule?.token) { if (gitlabHostRule?.token) {
const host = gitlabHostRule.resolvedHost ?? 'gitlab.com'; const host = gitlabHostRule.resolvedHost ?? 'gitlab.com';
authJson['gitlab-token'] = authJson['gitlab-token'] ?? {}; authJson['gitlab-token'] = authJson['gitlab-token'] ?? {};
@ -65,20 +75,24 @@ function getAuthJson(): string | null {
...(authJson['gitlab-domains'] ?? []), ...(authJson['gitlab-domains'] ?? []),
]; ];
} }
}); }
hostRules for (const packagistHostRule of hostRules.findAll({
.findAll({ hostType: PackagistDatasource.id }) hostType: PackagistDatasource.id,
?.forEach((hostRule) => { })) {
const { resolvedHost, username, password, token } = hostRule; if (!isArtifactAuthEnabled(packagistHostRule)) {
if (resolvedHost && username && password) { continue;
authJson['http-basic'] = authJson['http-basic'] ?? {}; }
authJson['http-basic'][resolvedHost] = { username, password };
} else if (resolvedHost && token) { const { resolvedHost, username, password, token } = packagistHostRule;
authJson.bearer = authJson.bearer ?? {}; if (resolvedHost && username && password) {
authJson.bearer[resolvedHost] = token; 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); return is.emptyObject(authJson) ? null : JSON.stringify(authJson);
} }

View file

@ -308,21 +308,26 @@ describe('modules/manager/composer/utils', () => {
matchHost: 'github.com', matchHost: 'github.com',
token: TOKEN_STRING, token: TOKEN_STRING,
}); });
expect(
findGithubToken({ const foundHostRule = hostRules.find({
hostType: GitTagsDatasource.id, hostType: GitTagsDatasource.id,
url: 'https://github.com', url: 'https://github.com',
}) });
).toEqual(TOKEN_STRING);
expect(findGithubToken(foundHostRule)).toEqual(TOKEN_STRING);
}); });
it('returns undefined when no hostRule match search', () => { it('returns undefined when no token is defined', () => {
expect( hostRules.add({
findGithubToken({ hostType: GitTagsDatasource.id,
hostType: GitTagsDatasource.id, matchHost: 'github.com',
url: 'https://github.com', });
})
).toBeUndefined(); const foundHostRule = hostRules.find({
hostType: GitTagsDatasource.id,
url: 'https://github.com',
});
expect(findGithubToken(foundHostRule)).toBeUndefined();
}); });
it('remove x-access-token token prefix', () => { it('remove x-access-token token prefix', () => {
@ -333,12 +338,12 @@ describe('modules/manager/composer/utils', () => {
matchHost: 'github.com', matchHost: 'github.com',
token: TOKEN_STRING_WITH_PREFIX, token: TOKEN_STRING_WITH_PREFIX,
}); });
expect(
findGithubToken({ const foundHostRule = hostRules.find({
hostType: GitTagsDatasource.id, hostType: GitTagsDatasource.id,
url: 'https://github.com', url: 'https://github.com',
}) });
).toEqual(TOKEN_STRING); expect(findGithubToken(foundHostRule)).toEqual(TOKEN_STRING);
}); });
}); });

View file

@ -3,8 +3,8 @@
import { quote } from 'shlex'; import { quote } from 'shlex';
import { GlobalConfig } from '../../../config/global'; import { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger'; import { logger } from '../../../logger';
import type { HostRuleSearchResult } from '../../../types';
import type { ToolConstraint } from '../../../util/exec/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 { api, id as composerVersioningId } from '../../versioning/composer';
import type { UpdateArtifactsConfig } from '../types'; import type { UpdateArtifactsConfig } from '../types';
import type { ComposerConfig, ComposerLock } from './types'; import type { ComposerConfig, ComposerLock } from './types';
@ -111,8 +111,10 @@ export function extractConstraints(
return res; return res;
} }
export function findGithubToken(search: HostRuleSearch): string | undefined { export function findGithubToken(
return findHostRule(search)?.token?.replace('x-access-token:', ''); searchResult: HostRuleSearchResult
): string | undefined {
return searchResult?.token?.replace('x-access-token:', '');
} }
export function isGithubPersonalAccessToken(token: string): boolean { export function isGithubPersonalAccessToken(token: string): boolean {
@ -173,3 +175,7 @@ export function takePersonalAccessTokenIfPossible(
return githubToken; return githubToken;
} }
export function isArtifactAuthEnabled(rule: HostRuleSearchResult): boolean {
return !rule.artifactAuth || rule.artifactAuth.includes('composer');
}

View file

@ -14,6 +14,7 @@ export interface HostRuleSearchResult {
dnsCache?: boolean; dnsCache?: boolean;
keepalive?: boolean; keepalive?: boolean;
artifactAuth?: string[] | null;
} }
export interface HostRule extends HostRuleSearchResult { export interface HostRule extends HostRuleSearchResult {