mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 15:01:49 +00:00
fix(util/host-rules): resolve enabled per trust tier
As part of follow-up changes, we're looking to tighten the way that
Renovate handles outgoing HTTP traffic, and a self-hosted administrator
disabling a host needs to be something a repository cannot undo.
In a similar change to
4732da7d2b, we can resolve `enabled` per
trust tier, so that a repository or preset rule can no longer re-enable
- or disable - a host the administrator's own rules set, which was
previously possible with a longer `matchHost`.
Within a tier nothing changes: the most specific rule still wins, so a
repository disabling a host of its own keeps working, as does an
administrator masking their own broad rule with a narrower one.
This is a behaviour change for repository config that previously relied
on overriding an admin rule's `enabled`.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
This commit is contained in:
@@ -2182,6 +2182,10 @@ To match specific ports you have to add a protocol to `matchHost`:
|
||||
Disabling a host is only 100% effective if added to self-hosted config.
|
||||
Renovate currently still checks its _cache_ for results first before trying to connect, so if a public host is blocked in your repository config (e.g. `renovate.json`) then it's possible you may get cached _results_ from that host if another repository using the same Renovate deployment has successfully queried for the same dependency recently.
|
||||
|
||||
!!! note
|
||||
`enabled` is not resolved by specificity alone: host rules from the self-hosted administrator's own config are resolved at a higher trust level than those from repository config or a preset.
|
||||
So if the administrator's own rules set `enabled` for a host, repository config or a preset cannot re-enable - or disable - that host, no matter how specific its `matchHost` is.
|
||||
|
||||
### `hostRules.abortIgnoreStatusCodes`
|
||||
|
||||
This field can be used to configure status codes that Renovate ignores and passes through when `abortOnError` is set to `true`.
|
||||
|
||||
@@ -690,6 +690,45 @@ describe('util/host-rules', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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('prefers the longest matchHost for a header they both set', () => {
|
||||
add({
|
||||
matchHost: 'https://registry.example.com',
|
||||
|
||||
@@ -258,6 +258,13 @@ function headersOfLastRuleToSetThem(
|
||||
.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* The last defined value, following the same "most specific rule wins" ordering as {@link headersOfLastRuleToSetThem}.
|
||||
*/
|
||||
function lastDefined<T>(values: (T | undefined)[]): T | undefined {
|
||||
return values.filter((value) => !isUndefined(value)).pop();
|
||||
}
|
||||
|
||||
export function find(search: HostRuleSearch): CombinedHostRule {
|
||||
if ([search.hostType, search.url].every(isFalsy)) {
|
||||
logger.warn({ search }, 'Invalid hostRules search');
|
||||
@@ -318,6 +325,17 @@ export function find(search: HostRuleSearch): CombinedHostRule {
|
||||
res.headers = { ...untrustedHeaders, ...trustedHeaders };
|
||||
}
|
||||
|
||||
// `enabled` is resolved per trust tier like `headers`: a repository or preset rule must not be able to re-enable a host the administrator's own rules disabled (or vice versa) by out-specifying them with a longer `matchHost`
|
||||
const trustedRules = matchedRules.filter((rule) => rule.trusted);
|
||||
const untrustedEnabled = lastDefined(
|
||||
matchedRules.filter((rule) => !rule.trusted).map((rule) => rule.enabled),
|
||||
);
|
||||
const trustedEnabled = lastDefined(trustedRules.map((rule) => rule.enabled));
|
||||
const enabled = trustedEnabled ?? untrustedEnabled;
|
||||
if (!isUndefined(enabled)) {
|
||||
res.enabled = enabled;
|
||||
}
|
||||
|
||||
delete res.hostType;
|
||||
delete res.resolvedHost;
|
||||
delete res.matchHost;
|
||||
|
||||
Reference in New Issue
Block a user