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.
### 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`.

View file

@ -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:

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 () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();

View file

@ -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,12 +75,16 @@ function getAuthJson(): string | null {
...(authJson['gitlab-domains'] ?? []),
];
}
});
}
hostRules
.findAll({ hostType: PackagistDatasource.id })
?.forEach((hostRule) => {
const { resolvedHost, username, password, token } = hostRule;
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 };
@ -78,7 +92,7 @@ function getAuthJson(): string | null {
authJson.bearer = authJson.bearer ?? {};
authJson.bearer[resolvedHost] = token;
}
});
}
return is.emptyObject(authJson) ? null : JSON.stringify(authJson);
}

View file

@ -308,21 +308,26 @@ describe('modules/manager/composer/utils', () => {
matchHost: 'github.com',
token: TOKEN_STRING,
});
expect(
findGithubToken({
const foundHostRule = hostRules.find({
hostType: GitTagsDatasource.id,
url: 'https://github.com',
})
).toEqual(TOKEN_STRING);
});
it('returns undefined when no hostRule match search', () => {
expect(
findGithubToken({
expect(findGithubToken(foundHostRule)).toEqual(TOKEN_STRING);
});
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',
})
).toBeUndefined();
});
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({
const foundHostRule = hostRules.find({
hostType: GitTagsDatasource.id,
url: 'https://github.com',
})
).toEqual(TOKEN_STRING);
});
expect(findGithubToken(foundHostRule)).toEqual(TOKEN_STRING);
});
});

View file

@ -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');
}

View file

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