Files
renovate/lib/util/host-rules.spec.ts
Jamie Tanna 7386ea49c4 refactor(host-rules): add an inherited trust tier
As part of looking to allow Inherit Config to manage Host Rules' grants,
we need to perform a prefactor to create an additional trust tier
between the global self-hosted administrator's config, and presets/repo
config.

The `find()` method already resolves sensitive fields - `headers`,
`enabled`, and the computed internal-host grant - a "trust tier" at a
time, so that the outcome cannot depend on how specific a rule from a
less trusted tier makes itself. The new `inherit` tier slots in between
the two we had:

- `headers` are applied untrusted first, then inherited, then the
  administrator's, so theirs still win outright
- `enabled` is taken from the administrator's rules, falling back to
  inherited config's, then a repository's
- an administrator's `allowInternal: false` acts as a veto: it is
  resolved before inherited config's rules, and clears the scoped
  and implicit grants too, so inherited config cannot re-grant the
  host in any shape by out-scoping it. Meanwhile any inherited rule
  naming a host in its `matchHost` - credentialed or not - now
  implicitly permits it, like the admin's own rules do

We introduce a separate field than `trusted` to avoid Inherit Config
gaining the same precedence as the admin themselves.

Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
2026-09-10 14:52:01 +01:00

1314 lines
40 KiB
TypeScript

import { logger, partial } from '~test/util.ts';
import { GlobalConfig } from '../config/global.ts';
import { NugetDatasource } from '../modules/datasource/nuget/index.ts';
import type { HostRule } from '../types/index.ts';
import {
type LegacyHostRule,
add,
clear,
confidentialFields,
filterAllowedHeaders,
find,
findAll,
getAll,
hostType,
hosts,
} from './host-rules.ts';
import { redactedFields, sanitize } from './sanitize.ts';
describe('util/host-rules', () => {
beforeEach(() => {
clear();
// `add()` filters `headers` against `allowedHeaders`
GlobalConfig.set({ allowedHeaders: ['X-*'] });
});
it('registers every redactedFields entry that is a HostRule field for value-level sanitizing', () => {
// exhaustive check for fields of `HostRule`, to introduce a compile-time error when adding a new field to `HostRule`
const allHostRuleFields: Record<keyof HostRule, true> = {
authType: true,
token: true,
username: true,
password: true,
insecureRegistry: true,
timeout: true,
abortOnError: true,
abortIgnoreStatusCodes: true,
allowInternal: true,
enabled: true,
enableHttp2: true,
concurrentRequestLimit: true,
maxRequestsPerSecond: true,
headers: true,
maxRetryAfter: true,
keepAlive: true,
artifactAuth: true,
httpsCertificateAuthority: true,
httpsPrivateKey: true,
httpsCertificate: true,
encrypted: true,
hostType: true,
matchHost: true,
resolvedHost: true,
readOnly: true,
};
const expectedConfidentialFields = redactedFields.filter(
(field) => field in allHostRuleFields,
);
expect([...confidentialFields].sort()).toEqual(
expectedConfidentialFields.sort(),
);
});
describe('add()', () => {
it('throws if both domainName and hostName', () => {
expect(() =>
add(
partial<LegacyHostRule & HostRule>({
hostType: 'azure',
domainName: 'github.com',
hostName: 'api.github.com',
}),
),
).toThrow(
'hostRules cannot contain more than one host-matching field - use',
);
});
it('throws if both domainName and baseUrl', () => {
expect(() =>
add(
partial<LegacyHostRule & HostRule>({
hostType: 'azure',
domainName: 'github.com',
matchHost: 'https://api.github.com',
}),
),
).toThrow(
'hostRules cannot contain more than one host-matching field - use',
);
});
it('throws if both hostName and baseUrl', () => {
expect(() =>
add(
partial<LegacyHostRule & HostRule>({
hostType: 'azure',
hostName: 'api.github.com',
matchHost: 'https://api.github.com',
}),
),
).toThrow(
'hostRules cannot contain more than one host-matching field - use',
);
});
it('supports baseUrl-only', () => {
add({
matchHost: 'https://some.endpoint',
username: 'user1',
password: 'pass1',
});
expect(find({ url: 'https://some.endpoint/v3/' })).toEqual({
password: 'pass1',
username: 'user1',
});
expect(find({ url: 'https://some.endpoint/' })).toEqual({
password: 'pass1',
username: 'user1',
});
expect(find({ url: 'https://some.endpoint' })).toEqual({
password: 'pass1',
username: 'user1',
});
expect(find({ url: 'https://some.endpoint:443' })).toEqual({
password: 'pass1',
username: 'user1',
});
});
it('does not match subpart of hostname', () => {
add({
matchHost: 'https://some.endpoint',
username: 'user1',
password: 'pass1',
});
expect(find({ url: 'https://some.endpoint.example.com' })).toEqual({});
expect(find({ url: 'https://some.endpoint:blub@example.com' })).toEqual(
{},
);
});
it('massages host url', () => {
add({
matchHost: 'some.domain.com:8080',
username: 'user1',
password: 'pass1',
});
add({
matchHost: 'domain.com/',
username: 'user2',
password: 'pass2',
});
expect(find({ url: 'https://some.domain.com:8080' })).toEqual({
password: 'pass1',
username: 'user1',
});
expect(find({ url: 'https://domain.com/' })).toEqual({
password: 'pass2',
username: 'user2',
});
});
it('sanitizes TLS credential values', () => {
add({
matchHost: 'https://some.endpoint',
httpsPrivateKey: 'private-key-value',
httpsCertificate: 'certificate-value',
httpsCertificateAuthority: 'certificate-authority-value',
});
expect(
sanitize(
'key=private-key-value cert=certificate-value ca=certificate-authority-value',
),
).toBe('key=**redacted** cert=**redacted** ca=**redacted**');
});
it('drops headers not permitted by allowedHeaders, with a warning', () => {
// enforced within `add()` itself, so that no registration path (config, .npmrc, a future caller) can bypass `allowedHeaders`
add({
matchHost: 'registry.example.com',
headers: { 'X-Allowed': 'yes', Authorization: 'denied' },
});
expect(find({ url: 'https://registry.example.com' })).toEqual({
headers: { 'X-Allowed': 'yes' },
});
expect(logger.logger.warn).toHaveBeenCalledWith(
{ denied: ['Authorization'] },
"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`",
);
});
it('drops all headers when allowedHeaders is unset (deny by default)', () => {
GlobalConfig.reset();
add({
matchHost: 'registry.example.com',
headers: { anything: 'x' },
});
expect(find({ url: 'https://registry.example.com' })).toEqual({});
});
it('prefers an explicitly-passed allowlist over GlobalConfig', () => {
// used when registering rules for a repository before `GlobalConfig` reflects it, i.e. a `repositories[]` entry's own `allowedHeaders` override
add(
{
matchHost: 'registry.example.com',
headers: { Authorization: 'from-admin', 'X-Dropped': 'yes' },
},
{ allowedHeaders: ['Authorization'] },
);
expect(find({ url: 'https://registry.example.com' })).toEqual({
headers: { Authorization: 'from-admin' },
});
expect(logger.logger.warn).toHaveBeenCalledWith(
{ denied: ['X-Dropped'] },
"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`",
);
});
});
describe('filterAllowedHeaders()', () => {
it('leaves rules without headers untouched', () => {
const rules = [{ matchHost: 'registry.example.com' }];
expect(filterAllowedHeaders(rules)).toEqual(rules);
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('keeps headers matching allowedHeaders and drops the rest with a warning', () => {
expect(
filterAllowedHeaders([
{
matchHost: 'registry.example.com',
headers: { 'X-Allowed': 'yes', Authorization: 'Bearer secret' },
},
]),
).toEqual([
{
matchHost: 'registry.example.com',
headers: { 'X-Allowed': 'yes' },
},
]);
expect(logger.logger.warn).toHaveBeenCalledWith(
{ denied: ['Authorization'] },
"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`",
);
});
it('leaves everything but the headers untouched', () => {
const rules = [
{
matchHost: 'registry.example.com',
hostType: 'npm',
username: 'user',
password: 'pass',
token: 'token',
headers: { 'X-Allowed': 'yes' },
},
];
expect(filterAllowedHeaders(rules)).toEqual(rules);
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('drops a header the self-hosted admin supplied themselves', () => {
// `allowedHeaders` enforces the checks regardless of whether it's global self-hosted administrator config, or repo config
GlobalConfig.reset();
expect(
filterAllowedHeaders([
{
matchHost: 'registry.example.com',
headers: { Authorization: 'set-by-admin' },
},
]),
// `headers` is dropped rather than left empty, so that the rule does not go on to suppress the headers of the broader rules it is combined with
).toEqual([{ matchHost: 'registry.example.com' }]);
expect(logger.logger.warn).toHaveBeenCalledWith(
{ denied: ['Authorization'] },
"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`",
);
});
it('prefers an explicitly-passed allowlist over GlobalConfig', () => {
expect(
filterAllowedHeaders(
[
{
matchHost: 'registry.example.com',
headers: { Authorization: 'from-admin', 'X-Dropped': 'yes' },
},
],
['Authorization'],
),
).toEqual([
{
matchHost: 'registry.example.com',
headers: { Authorization: 'from-admin' },
},
]);
expect(logger.logger.warn).toHaveBeenCalledWith(
{ denied: ['X-Dropped'] },
"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`",
);
});
});
describe('find()', () => {
beforeEach(() => {
clear();
});
it('warns and returns empty for bad search', () => {
// oxlint-disable-next-line renovate/prefer-partial-in-specs -- intentionally invalid search input
expect(find({ abc: 'def' } as any)).toEqual({});
});
it('needs exact host matches', () => {
add(
partial<LegacyHostRule & HostRule>({
hostType: NugetDatasource.id,
hostName: 'nuget.org',
username: 'root',
password: 'p4$$w0rd',
token: undefined,
}),
);
expect(find({ hostType: NugetDatasource.id })).toEqual({});
expect(
find({ hostType: NugetDatasource.id, url: 'https://nuget.org' }),
).not.toEqual({});
expect(
find({ hostType: NugetDatasource.id, url: 'https://not.nuget.org' }),
).not.toEqual({});
expect(
find({ hostType: NugetDatasource.id, url: 'https://not-nuget.org' }),
).toEqual({});
});
it('matches on empty rules', () => {
add({
enabled: true,
});
expect(
find({ hostType: NugetDatasource.id, url: 'https://api.github.com' }),
).toEqual({ enabled: true });
});
it('matches on hostType', () => {
add({
hostType: NugetDatasource.id,
token: 'abc',
});
expect(
find({ hostType: NugetDatasource.id, url: 'https://nuget.local/api' }),
).toEqual({ token: 'abc' });
});
it('matches on domainName', () => {
add(
partial<LegacyHostRule & HostRule>({
domainName: 'github.com',
token: 'def',
}),
);
expect(
find({ hostType: NugetDatasource.id, url: 'https://api.github.com' })
.token,
).toBe('def');
expect(
find({ hostType: NugetDatasource.id, url: 'https://github.com' }).token,
).toBe('def');
expect(
find({ hostType: NugetDatasource.id, url: 'https://apigithub.com' })
.token,
).toBeUndefined();
});
it('matches on specific path', () => {
// Initialized platform host rule
add({
hostType: 'github',
matchHost: 'https://api.github.com',
token: 'abc',
});
// specific host rule for using other token in different org
add({
hostType: 'github',
matchHost: 'https://api.github.com/repos/org-b/',
token: 'def',
});
// Initialized generic host rule for github platform
add({
hostType: 'github',
matchHost: 'https://api.github.com',
token: 'abc',
});
expect(
find({
hostType: 'github',
url: 'https://api.github.com/repos/org-b/someRepo/tags?per_page=100',
}).token,
).toBe('def');
});
it('matches for several hostTypes when no hostType rule is configured', () => {
add({
matchHost: 'https://api.github.com',
token: 'abc',
});
expect(
find({
hostType: 'github',
url: 'https://api.github.com/repos/org-b/someRepo/tags?per_page=100',
}).token,
).toBe('abc');
expect(
find({
hostType: 'github-releases',
url: 'https://api.github.com/repos/org-b/someRepo/tags?per_page=100',
}).token,
).toBe('abc');
});
it('matches if hostType is configured and host rule is filtered with datasource', () => {
add({
hostType: 'github',
matchHost: 'https://api.github.com',
token: 'abc',
});
add({
hostType: 'github-tags',
matchHost: 'https://api.github.com/repos/org-b/',
token: 'def',
});
expect(
find({
hostType: 'github-tags',
url: 'https://api.github.com/repos/org-b/someRepo/tags?per_page=100',
}).token,
).toBe('def');
});
it('matches on hostName', () => {
add(
partial<LegacyHostRule & HostRule>({
hostName: 'nuget.local',
token: 'abc',
}),
);
expect(
find({ hostType: NugetDatasource.id, url: 'https://nuget.local/api' }),
).toEqual({ token: 'abc' });
});
it('matches on matchHost with protocol', () => {
add({
matchHost: 'https://domain.com',
token: 'def',
});
expect(find({ url: 'https://api.domain.com' }).token).toBeUndefined();
expect(find({ url: 'https://domain.com' }).token).toBe('def');
expect(
find({
hostType: NugetDatasource.id,
url: 'https://domain.com/renovatebot',
}).token,
).toBe('def');
});
it('matches on matchHost without protocol', () => {
add({
matchHost: 'domain.com',
token: 'def',
});
expect(find({ url: 'https://api.domain.com' }).token).toBe('def');
expect(find({ url: 'https://domain.com' }).token).toBe('def');
expect(find({ url: 'httpsdomain.com' }).token).toBeUndefined();
});
it('matches on matchHost with dot prefix', () => {
add({
matchHost: '.domain.com',
token: 'def',
});
expect(find({ url: 'https://api.domain.com' }).token).toBe('def');
expect(find({ url: 'https://domain.com' }).token).toBeUndefined();
expect(find({ url: 'httpsdomain.com' }).token).toBeUndefined();
});
it('matches on matchHost with port', () => {
add({
matchHost: 'https://domain.com:9118',
token: 'def',
});
expect(find({ url: 'https://domain.com:9118' }).token).toBe('def');
expect(find({ url: 'https://domain.com' }).token).toBeUndefined();
expect(find({ url: 'httpsdomain.com' }).token).toBeUndefined();
});
it('matches on hostType and endpoint', () => {
add({
hostType: NugetDatasource.id,
matchHost: 'https://nuget.local/api',
token: 'abc',
});
expect(
find({ hostType: NugetDatasource.id, url: 'https://nuget.local/api' })
.token,
).toBe('abc');
});
it('matches on endpoint subresource', () => {
add({
hostType: NugetDatasource.id,
matchHost: 'https://nuget.local/api',
token: 'abc',
});
expect(
find({
hostType: NugetDatasource.id,
url: 'https://nuget.local/api/sub-resource',
}),
).toEqual({ token: 'abc' });
});
it('matches shortest matchHost first', () => {
add({
matchHost: 'https://nuget.local/api',
token: 'longest',
});
add({
matchHost: 'https://nuget.local/',
token: 'shortest',
});
expect(
find({
url: 'https://nuget.local/api/sub-resource',
}),
).toEqual({ token: 'longest' });
});
it('combines the headers of a trusted and an untrusted matching rule', () => {
// when setting `headers` in global self-hosted configuration and a repo, `find()` should combine the headers,
add(
{
matchHost: 'registry.example.com',
token: 'from-admin',
headers: { 'X-From-Admin': 'yes' },
},
{ trusted: true },
);
add({
matchHost: 'registry.example.com',
headers: { 'X-From-Repo': 'yes' },
});
expect(find({ url: 'https://registry.example.com' })).toEqual({
token: 'from-admin',
headers: { 'X-From-Admin': 'yes', 'X-From-Repo': 'yes' },
internalHostGrant: { implicit: true },
});
});
it('prefers a trusted rule for a header an untrusted rule also sets', () => {
// a repository must not be able to substitute the value the admin set, even with a more specific rule
add(
{
matchHost: 'registry.example.com',
headers: { 'X-Custom': 'from-admin' },
},
{ trusted: true },
);
add({
matchHost: 'https://registry.example.com/some/path',
headers: { 'X-Custom': 'from-repo' },
});
expect(
find({ url: 'https://registry.example.com/some/path/resource' }),
).toEqual({
headers: { 'X-Custom': 'from-admin' },
internalHostGrant: { implicit: true },
});
});
it('ignores a `trusted` smuggled in through the rule itself', () => {
add({
matchHost: 'registry.example.com',
headers: { 'X-Custom': 'from-admin' },
});
// `trusted` is not a `HostRule` field, but config is parsed from JSON, so a repository can still put one there
const smuggled: HostRule = {
matchHost: 'registry.example.com',
headers: { 'X-Custom': 'from-repo' },
};
add(Object.assign(smuggled, { trusted: true }));
// both rules are untrusted, so the second masks the first rather than being applied over it
expect(find({ url: 'https://registry.example.com' })).toEqual({
headers: { 'X-Custom': 'from-repo' },
});
});
it('lets the last matching rule of a tier mask the headers of a broader one', () => {
// the masking pattern: a broad rule carrying a credential, and a narrower rule that keeps it away from one host
add(
{
matchHost: 'example.com',
headers: { 'X-Api-Key': 'secret' },
},
{ trusted: true },
);
add(
{
matchHost: 'https://untrusted.example.com',
headers: { 'X-Other': 'yes' },
},
{ trusted: true },
);
expect(find({ url: 'https://untrusted.example.com' })).toEqual({
headers: { 'X-Other': 'yes' },
internalHostGrant: { implicit: true },
});
expect(find({ url: 'https://trusted.example.com' })).toEqual({
headers: { 'X-Api-Key': 'secret' },
internalHostGrant: { implicit: true },
});
});
it('masks within a tier without affecting the other tier', () => {
add(
{
matchHost: 'example.com',
headers: { 'X-From-Admin': 'yes' },
},
{ trusted: true },
);
add({
matchHost: 'example.com',
headers: { 'X-From-Repo': 'yes' },
});
add({
matchHost: 'https://untrusted.example.com',
headers: { 'X-Other-Repo-Header': 'yes' },
});
// the repo's narrower rule masks its own broader one, but cannot mask the admin's
expect(find({ url: 'https://untrusted.example.com' })).toEqual({
headers: { 'X-Other-Repo-Header': 'yes', 'X-From-Admin': 'yes' },
internalHostGrant: { implicit: true },
});
});
it('leaves a header name the admin masked from their own tier to the repository', () => {
// the admin's narrower rule masks their own broader one, so `X-From-Admin` is not among the headers they send to this host - which leaves the repository's value for that name the only one there is
add({ headers: { 'X-From-Admin': 'yes' } }, { trusted: true });
add(
{
matchHost: 'https://registry.example.com',
headers: { 'X-Other-Admin-Header': 'yes' },
},
{ trusted: true },
);
add({
matchHost: 'https://registry.example.com',
headers: { 'X-From-Admin': 'from-repo' },
});
expect(find({ url: 'https://registry.example.com' })).toEqual({
headers: { 'X-From-Admin': 'from-repo', 'X-Other-Admin-Header': 'yes' },
internalHostGrant: { implicit: true },
});
});
it('applies the headers of the three trust tiers in order', () => {
// the admin's own value must survive both of the tiers below theirs, and inherited config's must survive a repository's
add(
{
matchHost: 'registry.example.com',
headers: { 'X-Custom': 'from-admin' },
},
{ trusted: true },
);
add(
{
matchHost: 'https://registry.example.com/deeper',
headers: { 'X-Custom': 'from-inherited', 'X-Inherited': 'yes' },
},
{ inherited: true },
);
add({
matchHost: 'https://registry.example.com/deeper/still',
headers: {
'X-Custom': 'from-repo',
'X-Inherited': 'from-repo',
'X-Repo': 'yes',
},
});
expect(
find({ url: 'https://registry.example.com/deeper/still/resource' })
.headers,
).toEqual({
'X-Custom': 'from-admin',
'X-Inherited': 'yes',
'X-Repo': 'yes',
});
});
it('applies inherited headers over a repository rule when the admin sets none', () => {
add(
{
matchHost: 'registry.example.com',
headers: { 'X-Custom': 'from-inherited' },
},
{ inherited: true },
);
add({
matchHost: 'https://registry.example.com/deeper',
headers: { 'X-Custom': 'from-repo' },
});
expect(
find({ url: 'https://registry.example.com/deeper/resource' }).headers,
).toEqual({ 'X-Custom': 'from-inherited' });
});
it('does not let a rule whose headers were all denied suppress another rule of its tier', () => {
GlobalConfig.set({ allowedHeaders: ['X-*'] });
add({
matchHost: 'registry.example.com',
headers: { 'X-Custom': 'yes' },
});
add({
matchHost: 'https://registry.example.com/some/path',
headers: { Authorization: 'denied' },
});
expect(
find({ url: 'https://registry.example.com/some/path/resource' }),
).toEqual({
headers: { 'X-Custom': 'yes' },
});
});
describe('internalHostGrant', () => {
it('is absent when no trusted rule matches', () => {
add({ matchHost: 'registry.example.com', token: 'abc' });
expect(
find({ url: 'https://registry.example.com' }).internalHostGrant,
).toBeUndefined();
});
it('is implicit for a host the admin named', () => {
add(
{ matchHost: 'registry.example.com', token: 'abc' },
{ trusted: true },
);
expect(find({ url: 'https://registry.example.com' })).toEqual({
token: 'abc',
internalHostGrant: { implicit: true },
});
});
it('is not implicit for a host-less trusted rule', () => {
add({ hostType: 'nuget', token: 'abc' }, { trusted: true });
expect(
find({ hostType: 'nuget', url: 'https://internal.example.com' })
.internalHostGrant,
).toBeUndefined();
});
it('ignores allowInternal from untrusted config', () => {
add({ matchHost: 'internal.example.com', allowInternal: true });
expect(
find({ url: 'https://internal.example.com' }).internalHostGrant,
).toBeUndefined();
expect(logger.logger.debug).toHaveBeenCalledWith(
'Ignoring hostRules allowInternal for internal.example.com from untrusted config',
);
});
it('ignores allowInternal from an untrusted host-less rule', () => {
add({ hostType: 'nuget', allowInternal: true });
expect(
find({ hostType: 'nuget', url: 'https://internal.example.com' })
.internalHostGrant,
).toBeUndefined();
expect(logger.logger.debug).toHaveBeenCalledWith(
'Ignoring hostRules allowInternal for nuget from untrusted config',
);
});
it('takes explicit allowInternal from the admin, most specific rule winning', () => {
add(
{ matchHost: 'example.com', allowInternal: true },
{ trusted: true },
);
add(
{ matchHost: 'https://secure.example.com', allowInternal: false },
{ trusted: true },
);
expect(
find({ url: 'https://other.example.com' }).internalHostGrant,
).toEqual({ explicit: true, implicit: true });
expect(
find({ url: 'https://secure.example.com' }).internalHostGrant,
).toEqual({ explicit: false, scoped: false, implicit: false });
});
it('marks a grant scoped by hostType', () => {
add(
{
hostType: 'preset',
matchHost: 'presets.example.com',
allowInternal: true,
},
{ trusted: true },
);
expect(
find({ hostType: 'preset', url: 'https://presets.example.com' })
.internalHostGrant,
).toEqual({ explicit: true, scoped: true, implicit: true });
});
it('does not mark a bare-hostname grant as scoped', () => {
add(
{ matchHost: 'internal.example.com', allowInternal: true },
{ trusted: true },
);
expect(
find({ url: 'https://internal.example.com' }).internalHostGrant,
).toEqual({ explicit: true, implicit: true });
});
it('does not let a repository rule override the admin allowInternal', () => {
add(
{ matchHost: 'https://internal.example.com', allowInternal: false },
{ trusted: true },
);
add({
matchHost: 'https://internal.example.com/deeper/path',
allowInternal: true,
});
expect(
find({ url: 'https://internal.example.com/deeper/path/file' })
.internalHostGrant,
).toEqual({ explicit: false, scoped: false, implicit: false });
});
it('is implicit for a host named by trusted inherited config', () => {
add(
{ matchHost: 'registry.example.com', token: 'abc' },
{ inherited: true },
);
expect(find({ url: 'https://registry.example.com' })).toEqual({
token: 'abc',
internalHostGrant: { implicit: true },
});
});
it('takes explicit allowInternal from trusted inherited config', () => {
add(
{ matchHost: 'internal.example.com', allowInternal: true },
{ inherited: true },
);
expect(
find({ url: 'https://internal.example.com' }).internalHostGrant,
).toEqual({ explicit: true, implicit: true });
});
it('marks a grant scoped by trusted inherited config', () => {
add(
{
hostType: 'preset',
matchHost: 'https://presets.example.com/renovate/',
allowInternal: true,
},
{ inherited: true },
);
expect(
find({
hostType: 'preset',
url: 'https://presets.example.com/renovate/default',
}).internalHostGrant,
).toEqual({ explicit: true, scoped: true, implicit: true });
});
it('does not let inherited config override the admin allowInternal', () => {
// the admin's decision stands even though the inherited rule out-specifies theirs
add(
{ matchHost: 'https://internal.example.com', allowInternal: false },
{ trusted: true },
);
add(
{
hostType: 'preset',
matchHost: 'https://internal.example.com/deeper/path',
allowInternal: true,
},
{ inherited: true },
);
expect(
find({
hostType: 'preset',
url: 'https://internal.example.com/deeper/path/file',
}).internalHostGrant,
).toEqual({ explicit: false, scoped: false, implicit: false });
});
it('does not let an inherited scoped grant fill in for a bare admin veto', () => {
// the admin's rule sets no scoped verdict of its own, so a scoped inherited grant must not be the one that answers for it - config-fetching requests consult `scoped` alone
add(
{ matchHost: 'internal.example.com', allowInternal: false },
{ trusted: true },
);
add(
{
matchHost: 'https://internal.example.com/presets/',
allowInternal: true,
},
{ inherited: true },
);
expect(
find({ url: 'https://internal.example.com/presets/default' })
.internalHostGrant,
).toEqual({ explicit: false, scoped: false, implicit: false });
});
it('does not let an inherited bare grant fill in for a scoped admin veto', () => {
add(
{
matchHost: 'https://internal.example.com/presets/',
allowInternal: false,
},
{ trusted: true },
);
add(
{ matchHost: 'internal.example.com', allowInternal: true },
{ inherited: true },
);
expect(
find({ url: 'https://internal.example.com/presets/default' })
.internalHostGrant,
).toEqual({ explicit: false, scoped: false, implicit: false });
});
it('does not leave an implicit grant behind for a host the admin vetoed', () => {
add(
{ matchHost: 'internal.example.com', allowInternal: false },
{ trusted: true },
);
add(
{ matchHost: 'internal.example.com', token: 'abc' },
{ inherited: true },
);
expect(
find({ url: 'https://internal.example.com' }).internalHostGrant,
).toEqual({ explicit: false, scoped: false, implicit: false });
});
it('lets the admin mask their own allowInternal without vetoing', () => {
add(
{ matchHost: 'example.com', allowInternal: false },
{ trusted: true },
);
add(
{ matchHost: 'https://example.com/ok/', allowInternal: true },
{ trusted: true },
);
expect(
find({ url: 'https://example.com/ok/resource' }).internalHostGrant,
).toEqual({ explicit: true, scoped: true, implicit: true });
});
it('takes a scoped grant from inherited config when no admin rule matches', () => {
add(
{
matchHost: 'https://internal.example.com/presets/',
allowInternal: true,
},
{ inherited: true },
);
expect(
find({ url: 'https://internal.example.com/presets/default' })
.internalHostGrant,
).toEqual({ explicit: true, scoped: true, implicit: true });
});
});
describe('enabled trust tiers', () => {
it('does not let a more specific repository rule re-enable a host the admin disabled', () => {
add(
{ matchHost: 'bad.example.com', enabled: false },
{ trusted: true },
);
add({ matchHost: 'https://bad.example.com/deeper', enabled: true });
expect(
find({ url: 'https://bad.example.com/deeper/path' }).enabled,
).toBeFalse();
});
it('does not let a repository rule disable a host the admin enabled', () => {
add(
{ matchHost: 'good.example.com', enabled: true },
{ trusted: true },
);
add({ matchHost: 'https://good.example.com/deeper', enabled: false });
expect(
find({ url: 'https://good.example.com/deeper/path' }).enabled,
).toBeTrue();
});
it('keeps repository rules able to disable a host of their own', () => {
add({ matchHost: 'flaky.example.com', enabled: false });
expect(find({ url: 'https://flaky.example.com' }).enabled).toBeFalse();
});
it('lets the most specific rule of a tier win', () => {
add({ matchHost: 'example.com', enabled: false });
add({ matchHost: 'https://ok.example.com', enabled: true });
expect(find({ url: 'https://ok.example.com' }).enabled).toBeTrue();
});
it('does not let inherited config re-enable a host the admin disabled', () => {
add(
{ matchHost: 'bad.example.com', enabled: false },
{ trusted: true },
);
add(
{ matchHost: 'https://bad.example.com/deeper', enabled: true },
{ inherited: true },
);
expect(
find({ url: 'https://bad.example.com/deeper/path' }).enabled,
).toBeFalse();
});
it('does not let a repository rule re-enable a host inherited config disabled', () => {
add(
{ matchHost: 'bad.example.com', enabled: false },
{ inherited: true },
);
add({ matchHost: 'https://bad.example.com/deeper', enabled: true });
expect(
find({ url: 'https://bad.example.com/deeper/path' }).enabled,
).toBeFalse();
});
});
it('prefers the longest matchHost for a header they both set', () => {
add({
matchHost: 'https://registry.example.com',
headers: { 'X-Custom': 'shortest', 'X-Only-Shortest': 'yes' },
});
add({
matchHost: 'https://registry.example.com/some/path',
headers: { 'X-Custom': 'longest' },
});
expect(
find({ url: 'https://registry.example.com/some/path/resource' }),
).toEqual({
headers: { 'X-Custom': 'longest' },
});
});
it('prefers the longest matchHost even when a host-less rule is added between them', () => {
// the sort comparator must stay transitive: a rule with no `matchHost` compares equal to any other, so a naive length sort could leave the two `matchHost` rules unordered relative to each other
add({
matchHost: 'https://registry.example.com/some/path',
headers: { 'X-Custom': 'longest' },
});
add({
hostType: NugetDatasource.id,
token: 'unrelated',
});
add({
matchHost: 'https://registry.example.com',
headers: { 'X-Custom': 'shortest' },
});
expect(
find({ url: 'https://registry.example.com/some/path/resource' }),
).toEqual({
headers: { 'X-Custom': 'longest' },
});
});
it('prefers a `hostType` rule over one with a longer `matchHost`', () => {
// "most specific" is ranked before it is measured: a rule carrying both a `hostType` and a `matchHost` outranks a `matchHost`-only rule, however much more of the URL the latter matches
add({
matchHost: 'https://registry.example.com/some/path',
headers: { 'X-Custom': 'from-longer-matchHost' },
});
add({
hostType: NugetDatasource.id,
matchHost: 'https://registry.example.com',
headers: { 'X-Custom': 'from-hostType-rule' },
});
expect(
find({
url: 'https://registry.example.com/some/path/resource',
hostType: NugetDatasource.id,
}),
).toEqual({ headers: { 'X-Custom': 'from-hostType-rule' } });
});
it('keeps the headers of an earlier matching rule when a later one sets none', () => {
add({
matchHost: 'registry.example.com',
headers: { 'X-From-Admin': 'yes' },
});
add({
matchHost: 'registry.example.com',
timeout: 10000,
});
expect(find({ url: 'https://registry.example.com' })).toEqual({
headers: { 'X-From-Admin': 'yes' },
timeout: 10000,
});
});
it('matches readOnly requests', () => {
add({
matchHost: 'https://api.github.com/repos/',
token: 'aaa',
hostType: 'github',
});
add({
matchHost: 'https://api.github.com',
token: 'bbb',
readOnly: true,
});
expect(
find({
url: 'https://api.github.com/repos/foo/bar/tags',
readOnly: true,
}),
).toEqual({ token: 'bbb' });
});
});
describe('hosts()', () => {
it('returns hosts', () => {
add({
hostType: NugetDatasource.id,
token: 'aaaaaa',
});
add({
hostType: NugetDatasource.id,
matchHost: 'https://nuget.local/api',
token: 'abc',
});
add(
partial<LegacyHostRule & HostRule>({
hostType: NugetDatasource.id,
hostName: 'my.local.registry',
token: 'def',
}),
);
add({
hostType: NugetDatasource.id,
matchHost: 'another.local.registry',
token: 'xyz',
});
add({
hostType: NugetDatasource.id,
matchHost: 'https://yet.another.local.registry',
token: '123',
});
const res = hosts({
hostType: NugetDatasource.id,
});
expect(res).toEqual([
'nuget.local',
'my.local.registry',
'another.local.registry',
'yet.another.local.registry',
]);
});
});
describe('findAll()', () => {
it('warns and returns empty for bad search', () => {
// oxlint-disable-next-line renovate/prefer-partial-in-specs -- intentionally invalid search input
expect(findAll({ abc: 'def' } as any)).toEqual([]);
});
it('needs exact host matches', () => {
const hostRule = {
hostType: 'nuget',
hostName: 'nuget.org',
username: 'root',
password: 'p4$$w0rd',
};
add(hostRule);
expect(findAll({ hostType: 'nuget' })).toEqual([
{
hostType: 'nuget',
password: 'p4$$w0rd',
resolvedHost: 'nuget.org',
username: 'root',
matchHost: 'nuget.org',
trustTier: 'untrusted',
},
]);
});
});
describe('getAll()', () => {
it('returns all host rules', () => {
const hostRule1 = {
hostType: 'nuget',
matchHost: 'nuget.org',
username: 'root',
password: 'p4$$w0rd',
};
const hostRule2 = {
hostType: 'github',
matchHost: 'github.com',
token: 'token',
};
add(hostRule1);
add(hostRule2);
expect(getAll()).toMatchObject([hostRule1, hostRule2]);
});
});
describe('hostType()', () => {
it('return hostType', () => {
add({
hostType: 'github',
token: 'aaaaaa',
});
add({
hostType: 'github',
matchHost: 'github.example.com',
token: 'abc',
});
add({
hostType: 'github-changelog',
matchHost: 'https://github.example.com/chalk/chalk',
token: 'def',
});
expect(
hostType({
url: 'https://github.example.com/chalk/chalk',
}),
).toBe('github-changelog');
});
it('returns null', () => {
add({
hostType: 'github',
token: 'aaaaaa',
});
add({
hostType: 'github',
matchHost: 'github.example.com',
token: 'abc',
});
add({
hostType: 'github-changelog',
matchHost: 'https://github.example.com/chalk/chalk',
token: 'def',
});
expect(
hostType({
url: 'https://github.example.com/chalk/chalk',
}),
).toBe('github-changelog');
expect(
hostType({
url: 'https://gitlab.example.com/chalk/chalk',
}),
).toBeNull();
});
});
});