From bdabe43094c47d6c061fef05e0202d7d079e641e Mon Sep 17 00:00:00 2001
From: Yun Lai <ylai@squareup.com>
Date: Wed, 1 Nov 2023 00:04:30 +1100
Subject: [PATCH] feat(hostRules): support https options and platform in host
 rules from env (#25015)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 docs/usage/self-hosted-configuration.md       | 23 ++++++-
 .../config/parse/host-rules-from-env.spec.ts  | 42 +++++++++++++
 .../config/parse/host-rules-from-env.ts       | 62 +++++++++++++++++--
 3 files changed, 119 insertions(+), 8 deletions(-)

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index c92325ff76..c06bb004c6 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -266,11 +266,11 @@ If found, it will be imported into `config.npmrc` with `config.npmrcMerge` set t
 
 The format of the environment variables must follow:
 
-- Datasource name (e.g. `NPM`, `PYPI`)
+- Datasource name (e.g. `NPM`, `PYPI`) or Platform name (only `GITHUB`)
 - Underscore (`_`)
 - `matchHost`
 - Underscore (`_`)
-- Field name (`TOKEN`, `USERNAME`, or `PASSWORD`)
+- Field name (`TOKEN`, `USERNAME`, `PASSWORD`, `HTTPSPRIVATEKEY`, `HTTPSCERTIFICATE`, `HTTPSCERTIFICATEAUTHORITY`)
 
 Hyphens (`-`) in datasource or host name must be replaced with double underscores (`__`).
 Periods (`.`) in host names must be replaced with a single underscore (`_`).
@@ -278,6 +278,7 @@ Periods (`.`) in host names must be replaced with a single underscore (`_`).
 <!-- prettier-ignore -->
 !!! note
     You can't use these prefixes with the `detectHostRulesFromEnv` config option: `npm_config_`, `npm_lifecycle_`, `npm_package_`.
+    In addition, platform host rules will only be picked up when `matchHost` is supplied.
 
 ### npmjs registry token example
 
@@ -330,6 +331,24 @@ You can skip the host part, and use only the datasource and credentials.
 }
 ```
 
+### Platform with https authentication options
+
+`GITHUB_SOME_GITHUB__ENTERPRISE_HOST_HTTPSCERTIFICATE=certificate GITHUB_SOME_GITHUB__ENTERPRISE_HOST_HTTPSPRIVATEKEY=private-key GITHUB_SOME_GITHUB__ENTERPRISE_HOST_HTTPSCERTIFICATEAUTHORITY=certificate-authority`:
+
+```json
+{
+  "hostRules": [
+    {
+      "hostType": "github",
+      "matchHost": "some.github-enterprise.host",
+      "httpsPrivateKey": "private-key",
+      "httpsCertificate": "certificate",
+      "httpsCertificateAuthority": "certificate-authority"
+    }
+  ]
+}
+```
+
 ## dockerChildPrefix
 
 Adds a custom prefix to the default Renovate sidecar Docker containers name and label.
diff --git a/lib/workers/global/config/parse/host-rules-from-env.spec.ts b/lib/workers/global/config/parse/host-rules-from-env.spec.ts
index 39a624e074..a23234e4a1 100644
--- a/lib/workers/global/config/parse/host-rules-from-env.spec.ts
+++ b/lib/workers/global/config/parse/host-rules-from-env.spec.ts
@@ -51,6 +51,35 @@ describe('workers/global/config/parse/host-rules-from-env', () => {
     ]);
   });
 
+  it('support https authentication options', () => {
+    const envParam: NodeJS.ProcessEnv = {
+      GITHUB_SOME_GITHUB__ENTERPRISE_HOST_HTTPSPRIVATEKEY: 'private-key',
+      GITHUB_SOME_GITHUB__ENTERPRISE_HOST_HTTPSCERTIFICATE: 'certificate',
+      GITHUB_SOME_GITHUB__ENTERPRISE_HOST_HTTPSCERTIFICATEAUTHORITY:
+        'certificate-authority',
+    };
+    expect(hostRulesFromEnv(envParam)).toMatchObject([
+      {
+        hostType: 'github',
+        matchHost: 'some.github-enterprise.host',
+        httpsPrivateKey: 'private-key',
+        httpsCertificate: 'certificate',
+        httpsCertificateAuthority: 'certificate-authority',
+      },
+    ]);
+  });
+
+  it('make sure {{PLATFORM}}_TOKEN will not be picked up', () => {
+    const unsupportedEnv = ['GITHUB_TOKEN'];
+
+    for (const e of unsupportedEnv) {
+      const envParam: NodeJS.ProcessEnv = {
+        [e]: 'private-key',
+      };
+      expect(hostRulesFromEnv(envParam)).toMatchObject([]);
+    }
+  });
+
   it('supports datasource env token', () => {
     const envParam: NodeJS.ProcessEnv = {
       PYPI_TOKEN: 'some-token',
@@ -60,6 +89,19 @@ describe('workers/global/config/parse/host-rules-from-env', () => {
     ]);
   });
 
+  it('supports platform env token', () => {
+    const envParam: NodeJS.ProcessEnv = {
+      GITHUB_SOME_GITHUB__ENTERPRISE_HOST_TOKEN: 'some-token',
+    };
+    expect(hostRulesFromEnv(envParam)).toMatchObject([
+      {
+        hostType: 'github',
+        matchHost: 'some.github-enterprise.host',
+        token: 'some-token',
+      },
+    ]);
+  });
+
   it('rejects incomplete datasource env token', () => {
     const envParam: NodeJS.ProcessEnv = {
       PYPI_FOO_TOKEN: 'some-token',
diff --git a/lib/workers/global/config/parse/host-rules-from-env.ts b/lib/workers/global/config/parse/host-rules-from-env.ts
index 2db3e81a71..42b0c2928e 100644
--- a/lib/workers/global/config/parse/host-rules-from-env.ts
+++ b/lib/workers/global/config/parse/host-rules-from-env.ts
@@ -4,12 +4,57 @@ import type { HostRule } from '../../../../types';
 
 type AuthField = 'token' | 'username' | 'password';
 
+type HttpsAuthField =
+  | 'httpscertificate'
+  | 'httpsprivatekey'
+  | 'httpscertificateauthority';
+
 function isAuthField(x: unknown): x is AuthField {
   return x === 'token' || x === 'username' || x === 'password';
 }
 
+function isHttpsAuthField(x: unknown): x is HttpsAuthField {
+  return (
+    x === 'httpscertificate' ||
+    x === 'httpsprivatekey' ||
+    x === 'httpscertificateauthority'
+  );
+}
+
+function restoreHttpsAuthField(x: HttpsAuthField | AuthField): string {
+  switch (x) {
+    case 'httpsprivatekey':
+      return 'httpsPrivateKey';
+    case 'httpscertificate':
+      return 'httpsCertificate';
+    case 'httpscertificateauthority':
+      return 'httpsCertificateAuthority';
+  }
+
+  return x;
+}
+
+function setHostRuleValue(
+  rule: HostRule,
+  key: string,
+  value: string | undefined
+): void {
+  if (value !== undefined) {
+    switch (key) {
+      case 'token':
+      case 'username':
+      case 'password':
+      case 'httpsCertificateAuthority':
+      case 'httpsCertificate':
+      case 'httpsPrivateKey':
+        rule[key] = value;
+    }
+  }
+}
+
 export function hostRulesFromEnv(env: NodeJS.ProcessEnv): HostRule[] {
   const datasources = new Set(getDatasourceList());
+  const platforms = new Set(['github']);
 
   const hostRules: HostRule[] = [];
 
@@ -23,12 +68,17 @@ export function hostRulesFromEnv(env: NodeJS.ProcessEnv): HostRule[] {
     // Double underscore __ is used in place of hyphen -
     const splitEnv = envName.toLowerCase().replace(/__/g, '-').split('_');
     const hostType = splitEnv.shift()!;
-    if (datasources.has(hostType)) {
-      const suffix = splitEnv.pop()!;
-      if (isAuthField(suffix)) {
+    if (
+      datasources.has(hostType) ||
+      (platforms.has(hostType) && splitEnv.length > 1)
+    ) {
+      let suffix = splitEnv.pop()!;
+      if (isAuthField(suffix) || isHttpsAuthField(suffix)) {
+        suffix = restoreHttpsAuthField(suffix);
+
         let matchHost: string | undefined = undefined;
         const rule: HostRule = {};
-        rule[suffix] = env[envName];
+        setHostRuleValue(rule, suffix, env[envName]);
         if (splitEnv.length === 0) {
           // host-less rule
         } else if (splitEnv.length === 1) {
@@ -43,7 +93,7 @@ export function hostRulesFromEnv(env: NodeJS.ProcessEnv): HostRule[] {
         logger.debug(`Converting ${envName} into a global host rule`);
         if (existingRule) {
           // Add current field to existing rule
-          existingRule[suffix] = env[envName];
+          setHostRuleValue(existingRule, suffix, env[envName]);
         } else {
           // Create a new rule
           const newRule: HostRule = {
@@ -52,7 +102,7 @@ export function hostRulesFromEnv(env: NodeJS.ProcessEnv): HostRule[] {
           if (matchHost) {
             newRule.matchHost = matchHost;
           }
-          newRule[suffix] = env[envName];
+          setHostRuleValue(newRule, suffix, env[envName]);
           hostRules.push(newRule);
         }
       }
-- 
GitLab