Skip to content

Commit

Permalink
feat(platform): add client login to CLI
Browse files Browse the repository at this point in the history
* Added login and logout commands (not included in the docs for now)

* Auth logic for reading and storing client auth tokens (we use the
local SQLite DB for this).

* Auth logic for visiting the platform's login page, and for receiving
redirects from that page containing a valid client token.
  • Loading branch information
thsig committed Mar 30, 2020
1 parent 28be767 commit fabc472
Show file tree
Hide file tree
Showing 13 changed files with 440 additions and 37 deletions.
60 changes: 30 additions & 30 deletions garden-service/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion garden-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"node-forge": "^0.9.1",
"normalize-path": "^3.0.0",
"normalize-url": "^5.0.0",
"p-queue": "^6.3.0",
"open": "^7.0.2",
"p-retry": "^4.2.0",
"parse-git-config": "^3.0.0",
"path-is-inside": "^1.0.2",
Expand Down Expand Up @@ -129,6 +129,7 @@
"uuid": "^7.0.2",
"winston": "^3.2.1",
"wrap-ansi": "^6.2.0",
"ws": "^7.2.1",
"xml-js": "^1.6.11"
},
"devDependencies": {
Expand Down
5 changes: 4 additions & 1 deletion garden-service/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { resolve, join } from "path"
import { safeDump } from "js-yaml"
import { coreCommands } from "../commands/commands"
import { DeepPrimitiveMap } from "../config/common"
import { shutdown, sleep, getPackageVersion } from "../util/util"
import { shutdown, sleep, getPackageVersion, uuidv4 } from "../util/util"
import { deline } from "../util/string"
import {
BooleanParameter,
Expand Down Expand Up @@ -302,6 +302,8 @@ export class GardenCli {
logger.info("")
const footerLog = logger.placeholder()

const sessionId = uuidv4()

const contextOpts: GardenOpts = {
commandInfo: {
name: command.getFullName(),
Expand All @@ -310,6 +312,7 @@ export class GardenCli {
},
environmentName,
log,
sessionId,
}

let garden: Garden
Expand Down
4 changes: 4 additions & 0 deletions garden-service/src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { ServeCommand } from "./serve"
import { OptionsCommand } from "./options"
import { ConfigCommand } from "./config/config"
import { PluginsCommand } from "./plugins"
import { LoginCommand } from "./login"
import { LogOutCommand } from "./logout"

export const coreCommands: Command[] = [
new BuildCommand(),
Expand All @@ -55,4 +57,6 @@ export const coreCommands: Command[] = [
new UpdateRemoteCommand(),
new ValidateCommand(),
new ConfigCommand(),
new LoginCommand(),
new LogOutCommand(),
]
32 changes: 32 additions & 0 deletions garden-service/src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2018-2020 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 { Command, CommandParams, CommandResult } from "./base"
import { printHeader } from "../logger/util"
import dedent = require("dedent")
import { login } from "../platform/auth"

export class LoginCommand extends Command {
name = "login"
help = "Log in to Garden Cloud."
hidden = true

description = dedent`
Logs you in to Garden Cloud. Subsequent commands will have access to platform features.
`

async action({ garden, log, headerLog }: CommandParams): Promise<CommandResult> {
printHeader(headerLog, "Login", "cloud")

if (garden.platformUrl) {
await login(garden.platformUrl, log)
}

return {}
}
}
30 changes: 30 additions & 0 deletions garden-service/src/commands/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2018-2020 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 { Command, CommandParams, CommandResult } from "./base"
import { printHeader } from "../logger/util"
import dedent = require("dedent")
import { clearAuthToken } from "../platform/auth"

export class LogOutCommand extends Command {
name = "logout"
help = "Log out of Garden Cloud."
hidden = true

description = dedent`
Logs you out of Garden Cloud.
`

async action({ log, headerLog }: CommandParams): Promise<CommandResult> {
printHeader(headerLog, "Log out", "cloud")

await clearAuthToken(log)

return {}
}
}
8 changes: 8 additions & 0 deletions garden-service/src/db/base-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,12 @@ export class GardenEntity extends BaseEntity {
const connection = getConnection()
return connection.getRepository<T>(this)
}

/**
* Helper method to avoid circular import issues.
*/

static getConnection() {
return getConnection()
}
}
5 changes: 3 additions & 2 deletions garden-service/src/db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@
import { Connection, getConnectionManager, ConnectionOptions } from "typeorm"
import { join } from "path"
import { GARDEN_GLOBAL_PATH } from "../constants"
import { LocalAddress } from "./entities/local-address"

let connection: Connection

// Note: This function needs to be synchronous to work with the typeorm Active Record pattern (see ./base-entity.ts)
export function getConnection(): Connection {
if (!connection) {
const { LocalAddress } = require("./entities/local-address")
const { ClientAuthToken } = require("./entities/client-auth-token")
// Prepare the connection (the ormconfig.json in the static dir is only used for the typeorm CLI during dev)
const options: ConnectionOptions = {
type: "sqlite",
database: join(GARDEN_GLOBAL_PATH, "db"),
// IMPORTANT: All entities and migrations need to be manually referenced here because of how we
// package the garden binary
entities: [LocalAddress],
entities: [LocalAddress, ClientAuthToken],
migrations: [],
// Auto-create new tables on init
synchronize: true,
Expand Down
17 changes: 17 additions & 0 deletions garden-service/src/db/entities/client-auth-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2018-2020 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 { Entity, Column, Index } from "typeorm"
import { GardenEntity } from "../base-entity"

@Entity()
@Index(["token"], { unique: true })
export class ClientAuthToken extends GardenEntity {
@Column()
token: string
}
39 changes: 37 additions & 2 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { Module, getModuleCacheContext, getModuleKey, ModuleConfigMap } from "./
import { pluginModuleSchema, ModuleTypeMap } from "./types/plugin/plugin"
import { SourceConfig, ProjectConfig, resolveProjectConfig, pickEnvironment, OutputSpec } from "./config/project"
import { findByName, pickKeys, getPackageVersion, getNames, findByNames } from "./util/util"
import { ConfigurationError, PluginError, RuntimeError } from "./exceptions"
import { ConfigurationError, PluginError, RuntimeError, InternalError } from "./exceptions"
import { VcsHandler, ModuleVersion } from "./vcs/vcs"
import { GitHandler } from "./vcs/git"
import { BuildDir } from "./build-dir"
Expand Down Expand Up @@ -61,6 +61,7 @@ import { deline, naturalList } from "./util/string"
import { ensureConnected } from "./db/connection"
import { DependencyValidationGraph } from "./util/validate-dependencies"
import { Profile } from "./util/profiling"
import { readAuthToken, login } from "./platform/auth"

export interface ActionHandlerMap<T extends keyof PluginActionHandlers> {
[actionName: string]: PluginActionHandlers[T]
Expand Down Expand Up @@ -92,26 +93,30 @@ export interface GardenOpts {
persistent?: boolean
log?: LogEntry
plugins?: RegisterPluginParam[]
sessionId?: string
}

export interface GardenParams {
artifactsPath: string
buildDir: BuildDir
environmentName: string
clientAuthToken: string | null
dotIgnoreFiles: string[]
environmentName: string
gardenDirPath: string
log: LogEntry
moduleIncludePatterns?: string[]
moduleExcludePatterns?: string[]
opts: GardenOpts
outputs: OutputSpec[]
platformUrl: string | null
plugins: RegisterPluginParam[]
production: boolean
projectName: string
projectRoot: string
projectSources?: SourceConfig[]
providerConfigs: ProviderConfig[]
variables: DeepPrimitiveMap
sessionId: string | null
vcs: VcsHandler
workingCopyId: string
}
Expand All @@ -129,6 +134,11 @@ export class Garden {
private watcher: Watcher
private asyncLock: any

// Platform-related instance variables
public clientAuthToken: string | null
public platformUrl: string | null
public sessionId: string | null

public readonly configStore: ConfigStore
public readonly globalConfigStore: GlobalConfigStore
public readonly vcs: VcsHandler
Expand Down Expand Up @@ -158,6 +168,9 @@ export class Garden {

constructor(params: GardenParams) {
this.buildDir = params.buildDir
this.clientAuthToken = params.clientAuthToken
this.platformUrl = params.platformUrl
this.sessionId = params.sessionId
this.environmentName = params.environmentName
this.gardenDirPath = params.gardenDirPath
this.log = params.log
Expand Down Expand Up @@ -265,8 +278,30 @@ export class Garden {
// Connect to the state storage
await ensureConnected()

const sessionId = opts.sessionId || null

// TODO: Read the platformUrl from config.
const platformUrl = process.env.GARDEN_CLOUD ? `http://${process.env.GARDEN_CLOUD}` : null

const clientAuthToken = await readAuthToken(log)
// If a client auth token exists in local storage, we assume that the user wants to be logged in to the platform.
if (clientAuthToken && sessionId) {
if (!platformUrl) {
const errMsg = deline`
GARDEN_CLOUD environment variable is not set. Make sure it is set to the appropriate API
backend endpoint (e.g. myusername-cloud-api.cloud.dev.garden.io, without an http/https
prefix).`
throw new InternalError(errMsg, {})
} else {
await login(platformUrl, log)
}
}

const garden = new this({
artifactsPath,
clientAuthToken,
sessionId,
platformUrl,
projectRoot,
projectName,
environmentName,
Expand Down
Loading

0 comments on commit fabc472

Please sign in to comment.