From ace99fc08e11ec8f989f92e43bd8de5d656bfcf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BE=C3=B3r=20Magn=C3=BAsson?= Date: Tue, 16 Mar 2021 16:53:29 +0100 Subject: [PATCH] fix(core): fix logout when authenticated against different GE instance This really is an edge case and is mostly likely to happen during GE development when switching between different GE envs. --- core/src/cli/cli.ts | 4 +- core/src/commands/login.ts | 4 +- core/src/commands/logout.ts | 44 ++++++++++----- core/src/enterprise/api.ts | 46 +++++++--------- core/test/unit/src/commands/logout.ts | 77 +++++++++++++++++++++++++-- 5 files changed, 130 insertions(+), 45 deletions(-) diff --git a/core/src/cli/cli.ts b/core/src/cli/cli.ts index c3a8711d34..e01506b75f 100644 --- a/core/src/cli/cli.ts +++ b/core/src/cli/cli.ts @@ -211,7 +211,7 @@ ${renderCommands(commands)} // Init enterprise API let enterpriseApi: EnterpriseApi | null = null if (!command.noProject) { - enterpriseApi = await EnterpriseApi.factory(log, root) + enterpriseApi = await EnterpriseApi.factory({ log, currentDirectory: root }) } // Init event & log streaming. @@ -351,7 +351,7 @@ ${renderCommands(commands)} await bufferedEventStream.close() await dashboardEventStream.close() await command.server?.close() - await enterpriseApi?.close() + enterpriseApi?.close() } } } while (result.restartRequired) diff --git a/core/src/commands/login.ts b/core/src/commands/login.ts index 5c62292b7b..7811e995ce 100644 --- a/core/src/commands/login.ts +++ b/core/src/commands/login.ts @@ -39,10 +39,10 @@ export class LoginCommand extends Command { // The Enterprise API is missing from the Garden class for commands with noProject // so we initialize it here. - const enterpriseApi = await EnterpriseApi.factory(log, currentDirectory) + const enterpriseApi = await EnterpriseApi.factory({ log, currentDirectory, skipLogging: true }) if (enterpriseApi) { log.info({ msg: `You're already logged in to Garden Enteprise.` }) - await enterpriseApi.close() + enterpriseApi.close() return {} } diff --git a/core/src/commands/logout.ts b/core/src/commands/logout.ts index 2887c7a928..596ef5c95e 100644 --- a/core/src/commands/logout.ts +++ b/core/src/commands/logout.ts @@ -8,8 +8,9 @@ import { Command, CommandParams, CommandResult } from "./base" import { printHeader } from "../logger/util" -import dedent = require("dedent") import { EnterpriseApi } from "../enterprise/api" +import { ClientAuthToken } from "../db/entities/client-auth-token" +import { dedent } from "../util/string" export class LogOutCommand extends Command { name = "logout" @@ -26,24 +27,43 @@ export class LogOutCommand extends Command { } async action({ garden, log }: CommandParams): Promise { - // The Enterprise API is missing from the Garden class for commands with noProject - // so we initialize it here. - const enterpriseApi = await EnterpriseApi.factory(log, garden.projectRoot) - - if (!enterpriseApi) { + const token = await ClientAuthToken.findOne() + if (!token) { log.info({ msg: `You're already logged out from Garden Enterprise.` }) return {} } try { - await enterpriseApi.logout() - log.info({ msg: `Succesfully logged out from Garden Enterprise.` }) - } catch (error) { - log.error(error) - } + // The Enterprise API is missing from the Garden class for commands with noProject + // so we initialize it here. + const enterpriseApi = await EnterpriseApi.factory({ + log, + currentDirectory: garden.projectRoot, + skipLogging: true, + }) - await enterpriseApi.close() + if (!enterpriseApi) { + return {} + } + await enterpriseApi.post("token/logout", { + headers: { + Cookie: `rt=${token?.refreshToken}`, + }, + }) + enterpriseApi.close() + } catch (err) { + const msg = dedent` + The following issue occurred while logging out from Garden Enterprise (your session will be cleared regardless): ${err.message}\n + ` + log.warn({ + symbol: "warning", + msg, + }) + } finally { + log.info({ msg: `Succesfully logged out from Garden Enterprise.` }) + await EnterpriseApi.clearAuthToken(log) + } return {} } } diff --git a/core/src/enterprise/api.ts b/core/src/enterprise/api.ts index 3bca3ee176..96d36fd924 100644 --- a/core/src/enterprise/api.ts +++ b/core/src/enterprise/api.ts @@ -93,8 +93,19 @@ export class EnterpriseApi { * * Returns null if the project is not configured for Garden Enterprise or if the user is not logged in. * Throws if the user is logged in but the token is invalid and can't be refreshed. + * + * Optionally skip logging during initialization. Useful for noProject commands that need to use the class + * without all the "flair". */ - static async factory(log: LogEntry, currentDirectory: string) { + static async factory({ + log, + currentDirectory, + skipLogging = false, + }: { + log: LogEntry + currentDirectory: string + skipLogging?: boolean + }) { log.debug("Initializing enterprise API client.") const config = await getEnterpriseConfig(currentDirectory) @@ -112,7 +123,9 @@ export class EnterpriseApi { const api = new EnterpriseApi(log, config.domain, config.projectId) const tokenIsValid = await api.checkClientAuthToken() - const enterpriseLog = log.info({ section: "garden-enterprise", msg: "Connecting...", status: "active" }) + const enterpriseLog = skipLogging + ? null + : log.info({ section: "garden-enterprise", msg: "Connecting...", status: "active" }) if (gardenEnv.GARDEN_AUTH_TOKEN) { // Throw if using an invalid "CI" access token @@ -125,13 +138,13 @@ export class EnterpriseApi { } else { // Refresh the token if it's invalid. if (!tokenIsValid) { - enterpriseLog.debug({ msg: `Current auth token is invalid, refreshing` }) + enterpriseLog?.debug({ msg: `Current auth token is invalid, refreshing` }) try { // We can assert the token exsists since we're not using GARDEN_AUTH_TOKEN await api.refreshToken(token!) } catch (err) { - enterpriseLog.setError({ msg: `Invalid session`, append: true }) - enterpriseLog.warn(deline` + enterpriseLog?.setError({ msg: `Invalid session`, append: true }) + enterpriseLog?.warn(deline` Your session is invalid and could not be refreshed. If you were previously logged in to another instance of Garden Enterprise, please log out first and then log back in again. @@ -145,7 +158,7 @@ export class EnterpriseApi { api.startInterval() } - enterpriseLog.setSuccess({ msg: chalk.green("Ready"), append: true }) + enterpriseLog?.setSuccess({ msg: chalk.green("Ready"), append: true }) return api } @@ -232,7 +245,7 @@ export class EnterpriseApi { }, this.intervalMsec) } - async close() { + close() { if (this.intervalId) { clearInterval(this.intervalId) this.intervalId = null @@ -345,23 +358,4 @@ export class EnterpriseApi { this.log.debug(`Checked client auth token with platform - valid: ${valid}`) return valid } - - async logout() { - const token = await ClientAuthToken.findOne() - if (!token || gardenEnv.GARDEN_AUTH_TOKEN) { - // No op when the user is not logged in or an access token is in use - return - } - try { - await this.post("token/logout", { - headers: { - Cookie: `rt=${token?.refreshToken}`, - }, - }) - } catch (error) { - this.log.debug({ msg: `An error occurred while logging out from Garden Enterprise: ${error.message}` }) - } finally { - await EnterpriseApi.clearAuthToken(this.log) - } - } } diff --git a/core/test/unit/src/commands/logout.ts b/core/test/unit/src/commands/logout.ts index f6286de15f..c34048cb4d 100644 --- a/core/test/unit/src/commands/logout.ts +++ b/core/test/unit/src/commands/logout.ts @@ -38,7 +38,7 @@ describe("LogoutCommand", () => { await cleanupAuthTokens() }) - it("should logout from Gardne Enterprise", async () => { + it("should logout from Garden Enterprise", async () => { const postfix = randomString() const testToken = { token: `dummy-token-${postfix}`, @@ -52,7 +52,6 @@ describe("LogoutCommand", () => { commandInfo: { name: "foo", args: {}, opts: {} }, }) - // Save dummy token and mock some EnterpriesAPI methods await EnterpriseApi.saveAuthToken(garden.log, testToken) td.replace(EnterpriseApi.prototype, "checkClientAuthToken", async () => true) td.replace(EnterpriseApi.prototype, "startInterval", async () => {}) @@ -67,7 +66,10 @@ describe("LogoutCommand", () => { await command.action(makeCommandParams(garden)) const tokenAfterLogout = await ClientAuthToken.findOne() + const logOutput = getLogMessages(garden.log, (entry) => entry.level === LogLevel.info).join("\n") + expect(tokenAfterLogout).to.not.exist + expect(logOutput).to.include("Succesfully logged out from Garden Enterprise.") }) it("should be a no-op if the user is already logged out", async () => { @@ -80,7 +82,76 @@ describe("LogoutCommand", () => { await command.action(makeCommandParams(garden)) const logOutput = getLogMessages(garden.log, (entry) => entry.level === LogLevel.info).join("\n") - expect(logOutput).to.include("You're already logged out from Garden Enterprise.") }) + + it("should remove token even if Enterprise API can't be initialised", async () => { + const postfix = randomString() + const testToken = { + token: `dummy-token-${postfix}`, + refreshToken: `dummy-refresh-token-${postfix}`, + tokenValidity: 60, + } + + const command = new LogOutCommand() + const garden = await makeDummyGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { + noEnterprise: false, + commandInfo: { name: "foo", args: {}, opts: {} }, + }) + + await EnterpriseApi.saveAuthToken(garden.log, testToken) + // Throw when initializing Enterprise API + td.replace(EnterpriseApi.prototype, "factory", async () => { + throw new Error("Not tonight") + }) + + // Double check token actually exists + const savedToken = await ClientAuthToken.findOne() + expect(savedToken).to.exist + expect(savedToken!.token).to.eql(testToken.token) + expect(savedToken!.refreshToken).to.eql(testToken.refreshToken) + + await command.action(makeCommandParams(garden)) + + const tokenAfterLogout = await ClientAuthToken.findOne() + const logOutput = getLogMessages(garden.log, (entry) => entry.level === LogLevel.info).join("\n") + + expect(tokenAfterLogout).to.not.exist + expect(logOutput).to.include("Succesfully logged out from Garden Enterprise.") + }) + + it("should remove token even if API calls fail", async () => { + const postfix = randomString() + const testToken = { + token: `dummy-token-${postfix}`, + refreshToken: `dummy-refresh-token-${postfix}`, + tokenValidity: 60, + } + + const command = new LogOutCommand() + const garden = await makeDummyGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { + noEnterprise: false, + commandInfo: { name: "foo", args: {}, opts: {} }, + }) + + await EnterpriseApi.saveAuthToken(garden.log, testToken) + // Throw when using Enterprise API to call call logout endpoint + td.replace(EnterpriseApi.prototype, "post", async () => { + throw new Error("Not tonight") + }) + + // Double check token actually exists + const savedToken = await ClientAuthToken.findOne() + expect(savedToken).to.exist + expect(savedToken!.token).to.eql(testToken.token) + expect(savedToken!.refreshToken).to.eql(testToken.refreshToken) + + await command.action(makeCommandParams(garden)) + + const tokenAfterLogout = await ClientAuthToken.findOne() + const logOutput = getLogMessages(garden.log, (entry) => entry.level === LogLevel.info).join("\n") + + expect(tokenAfterLogout).to.not.exist + expect(logOutput).to.include("Succesfully logged out from Garden Enterprise.") + }) })