From f554cd24632c7977ef7ea21c5fdc43d7482b35aa Mon Sep 17 00:00:00 2001 From: elasticspoon Date: Wed, 3 Apr 2024 23:19:59 -0400 Subject: [PATCH 1/2] feat(cli): add auth command (#11004) Adds `coder auth` command with 3 sub-commands: `coder auth token`, `coder auth status` and `coder auth login`. `coder auth login`: This is the same command as `coder login`. `coder auth status`: Informs the user about their authentication status. It may make sense for this to not show an error if called when user is not logged in. `coder auth token`: Informs the user of their token string and the expiration time of the token. All the commands use a middleware to check that the user is authenticated and will return an error if the user is not. They also make at most 2 API calls. The commands have additional values that could be shown but are not with this implementation. --- cli/auth.go | 87 ++++++++++++++++++ cli/auth_test.go | 92 ++++++++++++++++++++ cli/root.go | 1 + cli/testdata/coder_--help.golden | 1 + cli/testdata/coder_auth_--help.golden | 14 +++ cli/testdata/coder_auth_login_--help.golden | 30 +++++++ cli/testdata/coder_auth_status_--help.golden | 9 ++ cli/testdata/coder_auth_token_--help.golden | 9 ++ codersdk/client.go | 2 +- docs/cli.md | 1 + docs/cli/auth.md | 19 ++++ docs/cli/auth_login.md | 57 ++++++++++++ docs/cli/auth_status.md | 11 +++ docs/cli/auth_token.md | 11 +++ docs/manifest.json | 20 +++++ 15 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 cli/auth.go create mode 100644 cli/auth_test.go create mode 100644 cli/testdata/coder_auth_--help.golden create mode 100644 cli/testdata/coder_auth_login_--help.golden create mode 100644 cli/testdata/coder_auth_status_--help.golden create mode 100644 cli/testdata/coder_auth_token_--help.golden create mode 100644 docs/cli/auth.md create mode 100644 docs/cli/auth_login.md create mode 100644 docs/cli/auth_status.md create mode 100644 docs/cli/auth_token.md diff --git a/cli/auth.go b/cli/auth.go new file mode 100644 index 0000000000000..1fecfba78a9ec --- /dev/null +++ b/cli/auth.go @@ -0,0 +1,87 @@ +package cli + +import ( + "fmt" + "strings" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" + "github.com/coder/serpent" +) + +func (r *RootCmd) auth() *serpent.Command { + cmd := &serpent.Command{ + Use: "auth ", + Short: "Manage information about internal authentication.", + Children: []*serpent.Command{ + r.authStatus(), + r.authToken(), + r.login(), + }, + Handler: func(inv *serpent.Invocation) error { + return inv.Command.HelpHandler(inv) + }, + } + return cmd +} + +func (r *RootCmd) authToken() *serpent.Command { + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "token", + Short: "Show session token value and expiration time.", + Middleware: serpent.Chain( + r.InitClient(client), + validateUserMW(client, r), + ), + Handler: func(inv *serpent.Invocation) error { + sessionID := strings.Split(client.SessionToken(), "-")[0] + key, err := client.APIKeyByID(inv.Context(), codersdk.Me, sessionID) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(inv.Stdout, "Your session token '%s' expires at %s.\n", client.SessionToken(), key.ExpiresAt) + + return nil + }, + } + return cmd +} + +func (r *RootCmd) authStatus() *serpent.Command { + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "status", + Short: "Show user authentication status.", + Middleware: serpent.Chain( + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + res, err := client.User(inv.Context(), codersdk.Me) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(inv.Stdout, "Hello there, %s! You're authenticated at %s.\n", pretty.Sprint(cliui.DefaultStyles.Keyword, res.Username), r.clientURL) + return nil + }, + } + return cmd +} + +func validateUserMW(client *codersdk.Client, _ *RootCmd) serpent.MiddlewareFunc { + return func(next serpent.HandlerFunc) serpent.HandlerFunc { + return func(inv *serpent.Invocation) error { + _, err := client.User(inv.Context(), codersdk.Me) + if err != nil { + return xerrors.Errorf("get user: %w", err) + } + + return next(inv) + } + } +} diff --git a/cli/auth_test.go b/cli/auth_test.go new file mode 100644 index 0000000000000..0e66335dec422 --- /dev/null +++ b/cli/auth_test.go @@ -0,0 +1,92 @@ +package cli_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/pty/ptytest" + "github.com/coder/coder/v2/testutil" +) + +func TestAuthToken(t *testing.T) { + t.Parallel() + t.Run("ValidUser", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + + inv, root := clitest.New(t, "auth", "token") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + split := strings.Split(client.SessionToken(), "-") + loginKey, err := client.APIKeyByID(ctx, codersdk.Me, split[0]) + require.NoError(t, err) + + doneChan := make(chan struct{}) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + pty.ExpectMatch(fmt.Sprintf("Your session token '%s' expires at %s.", client.SessionToken(), loginKey.ExpiresAt)) + <-doneChan + }) + + t.Run("NoUser", func(t *testing.T) { + t.Parallel() + inv, _ := clitest.New(t, "auth", "token") + + err := inv.Run() + errorMsg := "You are not logged in." + assert.ErrorContains(t, err, errorMsg) + }) +} + +func TestAuthStatus(t *testing.T) { + t.Parallel() + t.Run("ValidUser", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + + inv, root := clitest.New(t, "auth", "status") + clitest.SetupConfig(t, client, root) + + defaultUsername := "testuser" + + pty := ptytest.New(t).Attach(inv) + doneChan := make(chan struct{}) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + pty.ExpectMatch(fmt.Sprintf("Hello there, %s! You're authenticated at %s.", defaultUsername, client.URL.String())) + <-doneChan + }) + + t.Run("NoUser", func(t *testing.T) { + t.Parallel() + inv, _ := clitest.New(t, "auth", "status") + + err := inv.Run() + errorMsg := "You are not logged in." + assert.ErrorContains(t, err, errorMsg) + }) +} diff --git a/cli/root.go b/cli/root.go index d9407cf21766c..fcb57e553af84 100644 --- a/cli/root.go +++ b/cli/root.go @@ -82,6 +82,7 @@ const ( func (r *RootCmd) CoreSubcommands() []*serpent.Command { // Please re-sort this list alphabetically if you change it! return []*serpent.Command{ + r.auth(), r.dotfiles(), r.externalAuth(), r.login(), diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index e970347890eb2..a6256c88c42b9 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -14,6 +14,7 @@ USAGE: $ coder templates init SUBCOMMANDS: + auth Manage information about internal authentication. autoupdate Toggle auto-update policy for a workspace config-ssh Add an SSH Host entry for your workspaces "ssh coder.workspace" diff --git a/cli/testdata/coder_auth_--help.golden b/cli/testdata/coder_auth_--help.golden new file mode 100644 index 0000000000000..b7ae61d1302dc --- /dev/null +++ b/cli/testdata/coder_auth_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder auth + + Manage information about internal authentication. + +SUBCOMMANDS: + login Authenticate with Coder deployment + status Show user authentication status. + token Show session token value and expiration time. + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_auth_login_--help.golden b/cli/testdata/coder_auth_login_--help.golden new file mode 100644 index 0000000000000..420ee6bb774b5 --- /dev/null +++ b/cli/testdata/coder_auth_login_--help.golden @@ -0,0 +1,30 @@ +coder v0.0.0-devel + +USAGE: + coder auth login [flags] [] + + Authenticate with Coder deployment + +OPTIONS: + --first-user-email string, $CODER_FIRST_USER_EMAIL + Specifies an email address to use if creating the first user for the + deployment. + + --first-user-password string, $CODER_FIRST_USER_PASSWORD + Specifies a password to use if creating the first user for the + deployment. + + --first-user-trial bool, $CODER_FIRST_USER_TRIAL + Specifies whether a trial license should be provisioned for the Coder + deployment or not. + + --first-user-username string, $CODER_FIRST_USER_USERNAME + Specifies a username to use if creating the first user for the + deployment. + + --use-token-as-session bool + By default, the CLI will generate a new session token when logging in. + This flag will instead use the provided token as the session token. + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_auth_status_--help.golden b/cli/testdata/coder_auth_status_--help.golden new file mode 100644 index 0000000000000..aecda2ba6f52f --- /dev/null +++ b/cli/testdata/coder_auth_status_--help.golden @@ -0,0 +1,9 @@ +coder v0.0.0-devel + +USAGE: + coder auth status + + Show user authentication status. + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_auth_token_--help.golden b/cli/testdata/coder_auth_token_--help.golden new file mode 100644 index 0000000000000..c0cad4c82af92 --- /dev/null +++ b/cli/testdata/coder_auth_token_--help.golden @@ -0,0 +1,9 @@ +coder v0.0.0-devel + +USAGE: + coder auth token + + Show session token value and expiration time. + +——— +Run `coder --help` for a list of global options. diff --git a/codersdk/client.go b/codersdk/client.go index f1ac87981759b..a3e66ebc7a9f8 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -169,7 +169,7 @@ func (c *Client) SessionToken() string { return c.sessionToken } -// SetSessionToken returns the currently set token for the client. +// SetSessionToken sets sessionToken for the client. func (c *Client) SetSessionToken(token string) { c.mu.Lock() defer c.mu.Unlock() diff --git a/docs/cli.md b/docs/cli.md index 70dd29e28b9da..5231c035a0af9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -25,6 +25,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | Name | Purpose | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | +| [auth](./cli/auth.md) | Manage information about internal authentication. | | [dotfiles](./cli/dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository | | [external-auth](./cli/external-auth.md) | Manage external authentication | | [login](./cli/login.md) | Authenticate with Coder deployment | diff --git a/docs/cli/auth.md b/docs/cli/auth.md new file mode 100644 index 0000000000000..e119693a7bfa8 --- /dev/null +++ b/docs/cli/auth.md @@ -0,0 +1,19 @@ + + +# auth + +Manage information about internal authentication. + +## Usage + +```console +coder auth +``` + +## Subcommands + +| Name | Purpose | +| --------------------------------------- | --------------------------------------------- | +| [status](./auth_status.md) | Show user authentication status. | +| [token](./auth_token.md) | Show session token value and expiration time. | +| [login](./auth_login.md) | Authenticate with Coder deployment | diff --git a/docs/cli/auth_login.md b/docs/cli/auth_login.md new file mode 100644 index 0000000000000..86466fbb2367b --- /dev/null +++ b/docs/cli/auth_login.md @@ -0,0 +1,57 @@ + + +# auth login + +Authenticate with Coder deployment + +## Usage + +```console +coder auth login [flags] [] +``` + +## Options + +### --first-user-email + +| | | +| ----------- | ------------------------------------ | +| Type | string | +| Environment | $CODER_FIRST_USER_EMAIL | + +Specifies an email address to use if creating the first user for the deployment. + +### --first-user-username + +| | | +| ----------- | --------------------------------------- | +| Type | string | +| Environment | $CODER_FIRST_USER_USERNAME | + +Specifies a username to use if creating the first user for the deployment. + +### --first-user-password + +| | | +| ----------- | --------------------------------------- | +| Type | string | +| Environment | $CODER_FIRST_USER_PASSWORD | + +Specifies a password to use if creating the first user for the deployment. + +### --first-user-trial + +| | | +| ----------- | ------------------------------------ | +| Type | bool | +| Environment | $CODER_FIRST_USER_TRIAL | + +Specifies whether a trial license should be provisioned for the Coder deployment or not. + +### --use-token-as-session + +| | | +| ---- | ----------------- | +| Type | bool | + +By default, the CLI will generate a new session token when logging in. This flag will instead use the provided token as the session token. diff --git a/docs/cli/auth_status.md b/docs/cli/auth_status.md new file mode 100644 index 0000000000000..bcdb74f729eb7 --- /dev/null +++ b/docs/cli/auth_status.md @@ -0,0 +1,11 @@ + + +# auth status + +Show user authentication status. + +## Usage + +```console +coder auth status +``` diff --git a/docs/cli/auth_token.md b/docs/cli/auth_token.md new file mode 100644 index 0000000000000..fd3b8596011fb --- /dev/null +++ b/docs/cli/auth_token.md @@ -0,0 +1,11 @@ + + +# auth token + +Show session token value and expiration time. + +## Usage + +```console +coder auth token +``` diff --git a/docs/manifest.json b/docs/manifest.json index 6620160b0ff3e..6c0b4e41868b6 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -607,6 +607,26 @@ "path": "./cli.md", "icon_path": "./images/icons/terminal.svg", "children": [ + { + "title": "auth", + "description": "Manage information about internal authentication.", + "path": "cli/auth.md" + }, + { + "title": "auth login", + "description": "Authenticate with Coder deployment", + "path": "cli/auth_login.md" + }, + { + "title": "auth status", + "description": "Show user authentication status.", + "path": "cli/auth_status.md" + }, + { + "title": "auth token", + "description": "Show session token value and expiration time.", + "path": "cli/auth_token.md" + }, { "title": "autoupdate", "description": "Toggle auto-update policy for a workspace", From b15265333201b35f3481e229db033856d71d8e75 Mon Sep 17 00:00:00 2001 From: elasticspoon Date: Wed, 24 Apr 2024 19:59:00 -0400 Subject: [PATCH 2/2] fixup!: review changes Change language in commands Humanize session token remaining time Inline auth middleware --- cli/auth.go | 31 ++++++++++----------- cli/auth_test.go | 5 ++-- cli/testdata/coder_--help.golden | 2 +- cli/testdata/coder_auth_--help.golden | 4 +-- cli/testdata/coder_auth_token_--help.golden | 2 +- docs/cli.md | 2 +- docs/cli/auth.md | 12 ++++---- docs/cli/auth_token.md | 2 +- docs/manifest.json | 4 +-- 9 files changed, 31 insertions(+), 33 deletions(-) diff --git a/cli/auth.go b/cli/auth.go index 1fecfba78a9ec..147d89530c9a8 100644 --- a/cli/auth.go +++ b/cli/auth.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "strings" + "time" "golang.org/x/xerrors" @@ -15,7 +16,7 @@ import ( func (r *RootCmd) auth() *serpent.Command { cmd := &serpent.Command{ Use: "auth ", - Short: "Manage information about internal authentication.", + Short: "Manage authentication for Coder deployment.", Children: []*serpent.Command{ r.authStatus(), r.authToken(), @@ -32,19 +33,28 @@ func (r *RootCmd) authToken() *serpent.Command { client := new(codersdk.Client) cmd := &serpent.Command{ Use: "token", - Short: "Show session token value and expiration time.", + Short: "Show the current session token and expiration time.", Middleware: serpent.Chain( r.InitClient(client), - validateUserMW(client, r), ), Handler: func(inv *serpent.Invocation) error { + _, err := client.User(inv.Context(), codersdk.Me) + if err != nil { + return xerrors.Errorf("get user: %w", err) + } + sessionID := strings.Split(client.SessionToken(), "-")[0] key, err := client.APIKeyByID(inv.Context(), codersdk.Me, sessionID) if err != nil { return err } - _, _ = fmt.Fprintf(inv.Stdout, "Your session token '%s' expires at %s.\n", client.SessionToken(), key.ExpiresAt) + remainingHours := time.Until(key.ExpiresAt).Hours() + if remainingHours > 24 { + _, _ = fmt.Fprintf(inv.Stdout, "Your session token '%s' expires in %.1f days.\n", client.SessionToken(), remainingHours/24) + } else { + _, _ = fmt.Fprintf(inv.Stdout, "Your session token '%s' expires in %.1f hours.\n", client.SessionToken(), remainingHours) + } return nil }, @@ -72,16 +82,3 @@ func (r *RootCmd) authStatus() *serpent.Command { } return cmd } - -func validateUserMW(client *codersdk.Client, _ *RootCmd) serpent.MiddlewareFunc { - return func(next serpent.HandlerFunc) serpent.HandlerFunc { - return func(inv *serpent.Invocation) error { - _, err := client.User(inv.Context(), codersdk.Me) - if err != nil { - return xerrors.Errorf("get user: %w", err) - } - - return next(inv) - } - } -} diff --git a/cli/auth_test.go b/cli/auth_test.go index 0e66335dec422..3c204e528178d 100644 --- a/cli/auth_test.go +++ b/cli/auth_test.go @@ -32,7 +32,7 @@ func TestAuthToken(t *testing.T) { defer cancel() split := strings.Split(client.SessionToken(), "-") - loginKey, err := client.APIKeyByID(ctx, codersdk.Me, split[0]) + _, err := client.APIKeyByID(ctx, codersdk.Me, split[0]) require.NoError(t, err) doneChan := make(chan struct{}) @@ -42,7 +42,8 @@ func TestAuthToken(t *testing.T) { assert.NoError(t, err) }() - pty.ExpectMatch(fmt.Sprintf("Your session token '%s' expires at %s.", client.SessionToken(), loginKey.ExpiresAt)) + // token is valid for 24 hours by default + pty.ExpectMatch(fmt.Sprintf("Your session token '%s' expires in 24.0 hours.", client.SessionToken())) <-doneChan }) diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index a6256c88c42b9..614e1c441ba08 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -14,7 +14,7 @@ USAGE: $ coder templates init SUBCOMMANDS: - auth Manage information about internal authentication. + auth Manage authentication for Coder deployment. autoupdate Toggle auto-update policy for a workspace config-ssh Add an SSH Host entry for your workspaces "ssh coder.workspace" diff --git a/cli/testdata/coder_auth_--help.golden b/cli/testdata/coder_auth_--help.golden index b7ae61d1302dc..03180aaf77bca 100644 --- a/cli/testdata/coder_auth_--help.golden +++ b/cli/testdata/coder_auth_--help.golden @@ -3,12 +3,12 @@ coder v0.0.0-devel USAGE: coder auth - Manage information about internal authentication. + Manage authentication for Coder deployment. SUBCOMMANDS: login Authenticate with Coder deployment status Show user authentication status. - token Show session token value and expiration time. + token Show the current session token and expiration time. ——— Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_auth_token_--help.golden b/cli/testdata/coder_auth_token_--help.golden index c0cad4c82af92..d467ccf7c26ee 100644 --- a/cli/testdata/coder_auth_token_--help.golden +++ b/cli/testdata/coder_auth_token_--help.golden @@ -3,7 +3,7 @@ coder v0.0.0-devel USAGE: coder auth token - Show session token value and expiration time. + Show the current session token and expiration time. ——— Run `coder --help` for a list of global options. diff --git a/docs/cli.md b/docs/cli.md index 5231c035a0af9..d275ff9277c1a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -25,7 +25,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | Name | Purpose | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | -| [auth](./cli/auth.md) | Manage information about internal authentication. | +| [auth](./cli/auth.md) | Manage authentication for Coder deployment. | | [dotfiles](./cli/dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository | | [external-auth](./cli/external-auth.md) | Manage external authentication | | [login](./cli/login.md) | Authenticate with Coder deployment | diff --git a/docs/cli/auth.md b/docs/cli/auth.md index e119693a7bfa8..7332bae346962 100644 --- a/docs/cli/auth.md +++ b/docs/cli/auth.md @@ -2,7 +2,7 @@ # auth -Manage information about internal authentication. +Manage authentication for Coder deployment. ## Usage @@ -12,8 +12,8 @@ coder auth ## Subcommands -| Name | Purpose | -| --------------------------------------- | --------------------------------------------- | -| [status](./auth_status.md) | Show user authentication status. | -| [token](./auth_token.md) | Show session token value and expiration time. | -| [login](./auth_login.md) | Authenticate with Coder deployment | +| Name | Purpose | +| --------------------------------------- | --------------------------------------------------- | +| [status](./auth_status.md) | Show user authentication status. | +| [token](./auth_token.md) | Show the current session token and expiration time. | +| [login](./auth_login.md) | Authenticate with Coder deployment | diff --git a/docs/cli/auth_token.md b/docs/cli/auth_token.md index fd3b8596011fb..9d9bc481dd9e4 100644 --- a/docs/cli/auth_token.md +++ b/docs/cli/auth_token.md @@ -2,7 +2,7 @@ # auth token -Show session token value and expiration time. +Show the current session token and expiration time. ## Usage diff --git a/docs/manifest.json b/docs/manifest.json index 6c0b4e41868b6..97314b0f24972 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -609,7 +609,7 @@ "children": [ { "title": "auth", - "description": "Manage information about internal authentication.", + "description": "Manage authentication for Coder deployment.", "path": "cli/auth.md" }, { @@ -624,7 +624,7 @@ }, { "title": "auth token", - "description": "Show session token value and expiration time.", + "description": "Show the current session token and expiration time.", "path": "cli/auth_token.md" }, {