diff --git a/cli/auth.go b/cli/auth.go new file mode 100644 index 0000000000000..147d89530c9a8 --- /dev/null +++ b/cli/auth.go @@ -0,0 +1,84 @@ +package cli + +import ( + "fmt" + "strings" + "time" + + "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 authentication for Coder deployment.", + 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 the current session token and expiration time.", + Middleware: serpent.Chain( + r.InitClient(client), + ), + 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 + } + + 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 + }, + } + 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 +} diff --git a/cli/auth_test.go b/cli/auth_test.go new file mode 100644 index 0000000000000..3c204e528178d --- /dev/null +++ b/cli/auth_test.go @@ -0,0 +1,93 @@ +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(), "-") + _, 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) + }() + + // token is valid for 24 hours by default + pty.ExpectMatch(fmt.Sprintf("Your session token '%s' expires in 24.0 hours.", client.SessionToken())) + <-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..614e1c441ba08 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -14,6 +14,7 @@ USAGE: $ coder templates init SUBCOMMANDS: + 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 new file mode 100644 index 0000000000000..03180aaf77bca --- /dev/null +++ b/cli/testdata/coder_auth_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder auth + + Manage authentication for Coder deployment. + +SUBCOMMANDS: + login Authenticate with Coder deployment + status Show user authentication status. + 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_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..d467ccf7c26ee --- /dev/null +++ b/cli/testdata/coder_auth_token_--help.golden @@ -0,0 +1,9 @@ +coder v0.0.0-devel + +USAGE: + coder auth token + + Show the current session token 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..d275ff9277c1a 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 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 new file mode 100644 index 0000000000000..7332bae346962 --- /dev/null +++ b/docs/cli/auth.md @@ -0,0 +1,19 @@ + + +# auth + +Manage authentication for Coder deployment. + +## Usage + +```console +coder auth +``` + +## Subcommands + +| 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_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..9d9bc481dd9e4 --- /dev/null +++ b/docs/cli/auth_token.md @@ -0,0 +1,11 @@ + + +# auth token + +Show the current session token and expiration time. + +## Usage + +```console +coder auth token +``` diff --git a/docs/manifest.json b/docs/manifest.json index 6620160b0ff3e..97314b0f24972 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 authentication for Coder deployment.", + "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 the current session token and expiration time.", + "path": "cli/auth_token.md" + }, { "title": "autoupdate", "description": "Toggle auto-update policy for a workspace",