Skip to content

Commit

Permalink
fix(core): fix logout when authenticated against different GE instance
Browse files Browse the repository at this point in the history
This really is an edge case and is mostly likely to happen during GE
development when switching between different GE envs.
  • Loading branch information
eysi09 authored and thsig committed Mar 17, 2021
1 parent 51f4807 commit ace99fc
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 45 deletions.
4 changes: 2 additions & 2 deletions core/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -351,7 +351,7 @@ ${renderCommands(commands)}
await bufferedEventStream.close()
await dashboardEventStream.close()
await command.server?.close()
await enterpriseApi?.close()
enterpriseApi?.close()
}
}
} while (result.restartRequired)
Expand Down
4 changes: 2 additions & 2 deletions core/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}

Expand Down
44 changes: 32 additions & 12 deletions core/src/commands/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -26,24 +27,43 @@ export class LogOutCommand extends Command {
}

async action({ garden, log }: CommandParams): Promise<CommandResult> {
// 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 {}
}
}
46 changes: 20 additions & 26 deletions core/src/enterprise/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -232,7 +245,7 @@ export class EnterpriseApi {
}, this.intervalMsec)
}

async close() {
close() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
Expand Down Expand Up @@ -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)
}
}
}
77 changes: 74 additions & 3 deletions core/test/unit/src/commands/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand All @@ -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 () => {})
Expand All @@ -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 () => {
Expand All @@ -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.")
})
})

0 comments on commit ace99fc

Please sign in to comment.