Skip to content

Commit

Permalink
refactor(cloud): encapsulate backend-specific configs (#6710)
Browse files Browse the repository at this point in the history
* refactor(cloud): encapsulate backend-specific configs

To make `AuthRedirectServer` backend-agnostic.

Co-authored-by: Steffen Neubauer <[email protected]>

* refactor: move backend-specific machinery to own ts module

* chore: lint

---------

Co-authored-by: Steffen Neubauer <[email protected]>
  • Loading branch information
vvagaytsev and stefreak authored Dec 11, 2024
1 parent 62ddc3c commit a5783bd
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 39 deletions.
61 changes: 24 additions & 37 deletions core/src/cloud/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import type EventEmitter2 from "eventemitter2"
import bodyParser from "koa-bodyparser"
import Router from "koa-router"
import getPort from "get-port"
import cloneDeep from "fast-copy"
import type { Log } from "../logger/log-entry.js"
import { cloneDeep, isArray } from "lodash-es"
import { gardenEnv } from "../constants.js"
import type { GlobalConfigStore } from "../config-store/global.js"
import { dedent, deline } from "../util/string.js"
import { CloudApiError, InternalError } from "../exceptions.js"
import { add } from "date-fns"
import { getCloudDistributionName } from "./util.js"
import type { ParsedUrlQuery } from "node:querystring"

export interface AuthToken {
token: string
Expand Down Expand Up @@ -117,37 +118,34 @@ export const authTokenHeader =

export const makeAuthHeader = (clientAuthToken: string) => ({ [authTokenHeader]: clientAuthToken })

export type AuthRedirectServerConfig = {
events: EventEmitter2.EventEmitter2
log: Log
getLoginUrl: (port: number) => string
successUrl: string
extractAuthToken: (query: ParsedUrlQuery) => AuthToken
}

// TODO: Add analytics tracking
export class AuthRedirectServer {
private log: Log
private readonly log: Log

private server?: Server
private app?: Koa
private enterpriseDomain: string
private events: EventEmitter2.EventEmitter2

constructor(
enterpriseDomain: string,
events: EventEmitter2.EventEmitter2,
log: Log,
public port?: number
) {
this.enterpriseDomain = enterpriseDomain
this.events = events
this.log = log.createLog({})

constructor(private readonly config: AuthRedirectServerConfig) {
this.log = config.log.createLog({})
}

async start() {
if (this.app) {
return
}

if (!this.port) {
this.port = await getPort()
}
const port = await getPort()

await this.createApp()
const url = new URL(`/clilogin/${this.port}`, this.enterpriseDomain)
await open(url.href)
await this.createApp(port)
await open(this.config.getLoginUrl(port))
}

async close() {
Expand All @@ -160,23 +158,15 @@ export class AuthRedirectServer {
return undefined
}

async createApp() {
async createApp(port: number) {
const app = new Koa()
const http = new Router()

http.get("/", async (ctx) => {
const { jwt, rt, jwtval } = ctx.request.query
// TODO: validate properly
const tokenResponse: AuthToken = {
token: getFirstValue(jwt!),
refreshToken: getFirstValue(rt!),
tokenValidity: parseInt(getFirstValue(jwtval!), 10),
}
const tokenResponse = this.config.extractAuthToken(ctx.request.query)
this.log.debug("Received client auth token")
this.events.emit("receivedToken", tokenResponse)
ctx.redirect(`${this.enterpriseDomain}/clilogin/success`)
const url = new URL("/clilogin/success", this.enterpriseDomain)
ctx.redirect(url.href)
this.config.events.emit("receivedToken", tokenResponse)
ctx.redirect(this.config.successUrl)
})

app.use(bodyParser())
Expand All @@ -185,10 +175,7 @@ export class AuthRedirectServer {
app.on("error", (err) => {
this.log.error(`Auth redirect request failed with status ${err.status}: ${err.message}`)
})
this.server = app.listen(this.port)
this.server = app.listen(port)
this.app = app
}
}

function getFirstValue(v: string | string[]) {
return isArray(v) ? v[0] : v
}
41 changes: 41 additions & 0 deletions core/src/cloud/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2018-2024 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import type { AuthRedirectServerConfig } from "./auth.js"
import { isArray } from "lodash-es"

function getFirstValue(v: string | string[]) {
return isArray(v) ? v[0] : v
}

export type GardenBackendConfig = { readonly cloudDomain: string }
export type AuthRedirectConfig = Pick<AuthRedirectServerConfig, "getLoginUrl" | "successUrl" | "extractAuthToken">

export abstract class AbstractGardenBackend {
constructor(protected readonly config: GardenBackendConfig) {}

abstract getAuthRedirectConfig(): AuthRedirectConfig
}

export class GardenCloudBackend extends AbstractGardenBackend {
override getAuthRedirectConfig(): AuthRedirectConfig {
return {
getLoginUrl: (port) => new URL(`/clilogin/${port}`, this.config.cloudDomain).href,
successUrl: new URL("/clilogin/success", this.config.cloudDomain).href,
extractAuthToken: (query) => {
const { jwt, rt, jwtval } = query
// TODO: validate properly
return {
token: getFirstValue(jwt!),
refreshToken: getFirstValue(rt!),
tokenValidity: parseInt(getFirstValue(jwtval!), 10),
}
},
}
}
}
11 changes: 9 additions & 2 deletions core/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { printHeader } from "../logger/util.js"
import dedent from "dedent"
import { GardenCloudApi } from "../cloud/api.js"
import type { Log } from "../logger/log-entry.js"
import { ConfigurationError, TimeoutError, InternalError, CloudApiError } from "../exceptions.js"
import { CloudApiError, ConfigurationError, InternalError, TimeoutError } from "../exceptions.js"
import type { AuthToken } from "../cloud/auth.js"
import { AuthRedirectServer, saveAuthToken } from "../cloud/auth.js"
import type { EventBus } from "../events/events.js"
Expand All @@ -22,6 +22,7 @@ import { BooleanParameter } from "../cli/params.js"
import { deline } from "../util/string.js"
import { gardenEnv } from "../constants.js"
import { getCloudDistributionName, getGardenCloudDomain } from "../cloud/util.js"
import { GardenCloudBackend } from "../cloud/backend.js"

const loginTimeoutSec = 60

Expand Down Expand Up @@ -112,7 +113,13 @@ export class LoginCommand extends Command<{}, Opts> {

export async function login(log: Log, cloudDomain: string, events: EventBus) {
// Start auth redirect server and wait for its redirect handler to receive the redirect and finish running.
const server = new AuthRedirectServer(cloudDomain, events, log)
const gardenBackend = new GardenCloudBackend({ cloudDomain })
const server = new AuthRedirectServer({
events,
log,
...gardenBackend.getAuthRedirectConfig(),
})

const distroName = getCloudDistributionName(cloudDomain)
log.debug(`Redirecting to ${distroName} login page...`)
const response: AuthToken = await new Promise(async (resolve, reject) => {
Expand Down

0 comments on commit a5783bd

Please sign in to comment.