From 7d9c61a9928356307b09d7a1c566e4655d85a01c Mon Sep 17 00:00:00 2001 From: Maurice Faber Date: Thu, 7 Apr 2022 17:39:50 +0200 Subject: [PATCH] feat: host headers for internal requests (#67) to set host headers on keycloak backchannel requests --- src/tasks/gitea/gitea.ts | 4 ++-- src/tasks/harbor/harbor.ts | 2 +- src/tasks/keycloak/keycloak.ts | 14 +++++++++----- src/tasks/otomi/wait-for.ts | 6 +++--- src/utils.test.ts | 10 +++++----- src/utils.ts | 4 +++- src/validators.ts | 2 ++ 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/tasks/gitea/gitea.ts b/src/tasks/gitea/gitea.ts index 27743230..a11a40a4 100644 --- a/src/tasks/gitea/gitea.ts +++ b/src/tasks/gitea/gitea.ts @@ -1,7 +1,7 @@ -import { OrganizationApi, CreateRepoOption, CreateOrgOption, CreateTeamOption } from '@redkubes/gitea-client-node' +import { CreateOrgOption, CreateRepoOption, CreateTeamOption, OrganizationApi } from '@redkubes/gitea-client-node' import { doApiCall, waitTillAvailable } from '../../utils' import { cleanEnv, GITEA_PASSWORD, GITEA_URL } from '../../validators' -import { orgName, repoName, username, teamNameViewer } from '../common' +import { orgName, repoName, teamNameViewer, username } from '../common' const env = cleanEnv({ GITEA_PASSWORD, diff --git a/src/tasks/harbor/harbor.ts b/src/tasks/harbor/harbor.ts index 5e4409e0..b3363ba3 100644 --- a/src/tasks/harbor/harbor.ts +++ b/src/tasks/harbor/harbor.ts @@ -232,7 +232,7 @@ async function ensureTeamRobotAccountSecret(namespace: string, projectName): Pro async function main(): Promise { // harborHealthUrl is an in-cluster http svc, so no multiple external dns confirmations are needed - await waitTillAvailable(harborHealthUrl, { confirmations: 1 }) + await waitTillAvailable(harborHealthUrl, undefined, { confirmations: 1 }) const bearerAuth = await getBearerToken() robotApi.setDefaultAuthentication(bearerAuth) configureApi.setDefaultAuthentication(bearerAuth) diff --git a/src/tasks/keycloak/keycloak.ts b/src/tasks/keycloak/keycloak.ts index 7725e3d9..3c5778cf 100644 --- a/src/tasks/keycloak/keycloak.ts +++ b/src/tasks/keycloak/keycloak.ts @@ -20,7 +20,7 @@ import { UsersApi, } from '@redkubes/keycloak-client-node' import { forEach } from 'lodash' -import { Issuer, TokenSet } from 'openid-client' +import { custom, Issuer, TokenSet } from 'openid-client' import { doApiCall, handleErrors, waitTillAvailable } from '../../utils' import { cleanEnv, @@ -28,6 +28,7 @@ import { IDP_ALIAS, IDP_OIDC_URL, KEYCLOAK_ADDRESS, + KEYCLOAK_ADDRESS_INTERNAL, KEYCLOAK_ADMIN, KEYCLOAK_ADMIN_PASSWORD, KEYCLOAK_REALM, @@ -52,6 +53,7 @@ const env = cleanEnv({ KEYCLOAK_ADMIN, KEYCLOAK_ADMIN_PASSWORD, KEYCLOAK_ADDRESS, + KEYCLOAK_ADDRESS_INTERNAL, KEYCLOAK_REALM, FEAT_EXTERNAL_IDP, }) @@ -59,12 +61,14 @@ const env = cleanEnv({ const errors: string[] = [] async function main(): Promise { - await waitTillAvailable(env.KEYCLOAK_ADDRESS) - const keycloakAddress = env.KEYCLOAK_ADDRESS - const basePath = `${keycloakAddress}/admin/realms` + await waitTillAvailable(env.KEYCLOAK_ADDRESS_INTERNAL) + const keycloakSvc = env.KEYCLOAK_ADDRESS_INTERNAL || 'http://keycloak-http.keycloak' + const basePath = `${keycloakSvc}/admin/realms` let token: TokenSet try { - const keycloakIssuer = await Issuer.discover(`${keycloakAddress}/realms/${env.KEYCLOAK_REALM}/`) + custom.setHttpOptionsDefaults({ headers: { host: env.KEYCLOAK_ADDRESS.replace('https://', '') } }) + const keycloakIssuer = await Issuer.discover(`${keycloakSvc}/realms/${env.KEYCLOAK_REALM}/`) + // console.log(keycloakIssuer) const clientOptions: any = { client_id: 'admin-cli', client_secret: 'unused', diff --git a/src/tasks/otomi/wait-for.ts b/src/tasks/otomi/wait-for.ts index 8031de3e..be773897 100644 --- a/src/tasks/otomi/wait-for.ts +++ b/src/tasks/otomi/wait-for.ts @@ -1,8 +1,8 @@ import { waitTillAvailable } from '../../utils' -import { cleanEnv, WAIT_OPTIONS, WAIT_URL } from '../../validators' +import { cleanEnv, WAIT_OPTIONS, WAIT_URL, WAIT_HOST } from '../../validators' -const env = cleanEnv({ WAIT_OPTIONS, WAIT_URL }) +const env = cleanEnv({ WAIT_OPTIONS, WAIT_URL, WAIT_HOST }) if (typeof require !== 'undefined' && require.main === module) { - waitTillAvailable(env.WAIT_URL, env.WAIT_OPTIONS) + waitTillAvailable(env.WAIT_URL, env.WAIT_HOST, env.WAIT_OPTIONS) } diff --git a/src/utils.test.ts b/src/utils.test.ts index 58866118..27379ffc 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -46,7 +46,7 @@ describe('utils', () => { it('should pass after x successful requests', async () => { const stub = sandbox.stub(fetch, 'Promise').returns(successResp) const confirmations = 3 - const res = waitTillAvailable(url, { confirmations }) + const res = waitTillAvailable(url, undefined, { confirmations }) await tick(confirmations + 2) // wait extra rounds expect(stub).to.have.callCount(confirmations) await expect(res).to.eventually.be.fulfilled @@ -55,7 +55,7 @@ describe('utils', () => { it('should reset confirmation counter', async () => { const stub = sandbox.stub(fetch, 'Promise').returns(successResp) const confirmations = 3 - const res = waitTillAvailable(url, { confirmations }) + const res = waitTillAvailable(url, undefined, { confirmations }) await tick(confirmations + 2) // wait extra rounds expect(stub).to.have.callCount(confirmations) await expect(res).to.eventually.be.fulfilled @@ -64,7 +64,7 @@ describe('utils', () => { it('should bail when a request returns an unexpected status code', async () => { const stub = sandbox.stub(fetch, 'Promise').returns(failResp) const retries = 3 - const res = waitTillAvailable(url, { retries }) + const res = waitTillAvailable(url, undefined, { retries }) await tick(retries + 1) expect(stub).to.have.callCount(1) await expect(res).to.eventually.be.rejectedWith(`Wrong status code: 500`) @@ -74,7 +74,7 @@ describe('utils', () => { const stub = sandbox.stub(fetch, 'Promise').throws(new Error('ECONNREFUSED')) const retries = 3 const maxTimeout = 30000 - const res = waitTillAvailable(url, { retries, maxTimeout }) + const res = waitTillAvailable(url, undefined, { retries, maxTimeout }) await tick(retries + 2, maxTimeout) // run a couple extra rounds and set duration to maxTimeout to make sure we have spent enough time expect(stub).to.have.callCount(3) await expect(res).to.eventually.be.rejectedWith(`Max retries (${retries}) has been reached!`) @@ -85,7 +85,7 @@ describe('utils', () => { const confirmations = 3 const retries = 1000 // large enough const maxTimeout = 1000 // same as minTimeout to be able to calculate attempts - const res = waitTillAvailable(url, { confirmations, retries, maxTimeout, forever: true }) + const res = waitTillAvailable(url, undefined, { confirmations, retries, maxTimeout, forever: true }) // tick 5 failures await tick(5) // now start returning ok responses diff --git a/src/utils.ts b/src/utils.ts index 6281d4ea..ca2345d8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import retry, { Options } from 'async-retry' import http, { Agent as AgentHttp } from 'http' import { Agent } from 'https' +import { set } from 'lodash' import fetch, { RequestInit } from 'node-fetch' import { cleanEnv, NODE_EXTRA_CA_CERTS, NODE_TLS_REJECT_UNAUTHORIZED } from './validators' @@ -76,7 +77,7 @@ const sleep = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)) } -export const waitTillAvailable = async (url: string, opts?: WaitTillAvailableOptions): Promise => { +export const waitTillAvailable = async (url: string, host?: string, opts?: WaitTillAvailableOptions): Promise => { const options: WaitTillAvailableOptions = { ...defaultOptions, ...opts } if (env.isDev) { options.confirmations = 1 @@ -89,6 +90,7 @@ export const waitTillAvailable = async (url: string, opts?: WaitTillAvailableOpt : new AgentHttp(), timeout: 5000, } + if (host) set(fetchOptions, 'headers.host', host) // we don't trust dns in the cluster and want a lot of confirmations // but we don't care about those when we call the cluster from outside diff --git a/src/validators.ts b/src/validators.ts index ebcab771..624df26b 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -43,6 +43,7 @@ export const DRONE_URL = str({ desc: 'The public url of the drone server' }) export const GITEA_PASSWORD = str({ desc: 'The gitea admin password' }) export const GITEA_URL = url({ desc: 'The gitea core service url' }) export const KEYCLOAK_ADDRESS = str({ desc: 'The Keycloak Server address' }) +export const KEYCLOAK_ADDRESS_INTERNAL = str({ desc: 'The internal Keycloak kubernetes svc address' }) export const KEYCLOAK_ADMIN = str({ desc: 'Default admin username for KeyCloak Server', default: 'admin' }) export const KEYCLOAK_ADMIN_PASSWORD = str({ desc: 'Default password for admin' }) export const KEYCLOAK_CLIENT_ID = str({ desc: 'Default Keycloak Client', default: 'otomi' }) @@ -63,6 +64,7 @@ export const REGION = str({ desc: 'The cloud region' }) export const SECRETS_NAMESPACE = str({ desc: 'The namespace of the TLS secrets', default: 'istio-system' }) export const TEAM_IDS = json({ desc: 'A list of team ids in JSON format' }) export const WAIT_URL = str({ desc: 'The URL to wait for.' }) +export const WAIT_HOST = str({ desc: 'The HOST header that goes with the url to wait for.', default: undefined }) export const WAIT_OPTIONS = json({ desc: 'The waitTillAvailable options', default: '{}' }) // set default to undefined based on feature flags: