From 6c50b85a129df76fb320f588d20458de42b09f86 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 16 Feb 2022 01:09:15 +0000 Subject: [PATCH 01/17] Create initial route for generating an API key --- coderd/coderd.go | 7 +++++++ coderd/users.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/coderd/coderd.go b/coderd/coderd.go index 9669dbf92c2e1..6409aa3f55c20 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -36,6 +36,13 @@ func New(options *Options) http.Handler { }) r.Post("/login", api.postLogin) r.Post("/logout", api.postLogout) + r.Route("/api-keys", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + ) + r.Post("/", api.postApiKey) + }) + // Used for setup. r.Get("/user", api.user) r.Post("/user", api.postUser) diff --git a/coderd/users.go b/coderd/users.go index ab6328fbe1984..87a46aeca7366 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -55,6 +55,11 @@ type LoginWithPasswordResponse struct { SessionToken string `json:"session_token" validate:"required"` } +// GenerateAPIKeyResponse contains an API key for a user. +type GenerateAPIKeyResponse struct { + Key string `json:"key"` +} + // Returns whether the initial user has been created or not. func (api *api) user(rw http.ResponseWriter, r *http.Request) { userCount, err := api.Database.GetUserCount(r.Context()) @@ -312,6 +317,43 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { }) } +// Creates a new API key, used for logging in via the CLI +func (api *api) postApiKey(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + userID := apiKey.UserID + + keyID, keySecret, err := generateAPIKeyIDSecret() + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("generate api key parts: %s", err.Error()), + }) + return + } + hashed := sha256.Sum256([]byte(keySecret)) + + _, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ + ID: keyID, + UserID: userID, + ExpiresAt: database.Now().AddDate(1, 0, 0), // Expire after 1 year (same as v1) + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + HashedSecret: hashed[:], + LoginType: database.LoginTypeBuiltIn, + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("insert api key: %s", err.Error()), + }) + return + } + + // This format is consumed by the APIKey middleware. + generatedApiKey := fmt.Sprintf("%s-%s", keyID, keySecret) + + render.Status(r, http.StatusCreated) + render.JSON(rw, r, GenerateAPIKeyResponse{Key: generatedApiKey}) +} + // Clear the user's session cookie func (*api) postLogout(rw http.ResponseWriter, r *http.Request) { // Get a blank token cookie From fe55c73edb5398b1e3558a7a33a8ae3f7798687e Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 16 Feb 2022 01:44:07 +0000 Subject: [PATCH 02/17] Add cli-auth page --- site/api.ts | 13 ++++++++++ site/pages/cli-auth.tsx | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 site/pages/cli-auth.tsx diff --git a/site/api.ts b/site/api.ts index 0a11659ed24d7..eb37583d262e7 100644 --- a/site/api.ts +++ b/site/api.ts @@ -139,3 +139,16 @@ export const logout = async (): Promise => { return } + +export const getApiKey = async (): Promise<{ key: string }> => { + const response = await fetch("/api/v2/api-keys", { + method: "POST", + }) + + if (!response.ok) { + const body = await response.json() + throw new Error(body.message) + } + + return await response.json() +} diff --git a/site/pages/cli-auth.tsx b/site/pages/cli-auth.tsx new file mode 100644 index 0000000000000..2c796fc0f7e53 --- /dev/null +++ b/site/pages/cli-auth.tsx @@ -0,0 +1,56 @@ +import { Typography } from "@material-ui/core" +import Paper from "@material-ui/core/Paper" +import { makeStyles } from "@material-ui/core/styles" +import React, { useEffect, useState } from "react" +import { getApiKey } from "../api" +import { CodeExample } from "../components/CodeExample" + +import { FullScreenLoader } from "../components/Loader/FullScreenLoader" +import { useUser } from "../contexts/UserContext" + +const CliAuthenticationPage: React.FC = () => { + const { me } = useUser(true) + const styles = useStyles() + + const [apiKey, setApiKey] = useState(null) + + useEffect(() => { + if (me?.id) { + getApiKey().then(({ key }) => { + setApiKey(key) + }) + } + }, [me?.id]) + + if (!apiKey) { + return + } + + return ( +
+ + Session Token + + +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + root: { + width: "100vh", + height: "100vw", + display: "flex", + justifyContent: "center", + alignItems: "center", + }, + title: { + marginBottom: theme.spacing(2), + }, + container: { + maxWidth: "680px", + padding: theme.spacing(2), + }, +})) + +export default CliAuthenticationPage From 3bb3164dd9d028d8489d5ce98a6e06e6afbd631d Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 16 Feb 2022 02:15:16 +0000 Subject: [PATCH 03/17] Fix lint warnings --- site/pages/cli-auth.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/pages/cli-auth.tsx b/site/pages/cli-auth.tsx index 2c796fc0f7e53..0df7147d9c4ff 100644 --- a/site/pages/cli-auth.tsx +++ b/site/pages/cli-auth.tsx @@ -1,5 +1,5 @@ -import { Typography } from "@material-ui/core" import Paper from "@material-ui/core/Paper" +import Typography from "@material-ui/core/Typography" import { makeStyles } from "@material-ui/core/styles" import React, { useEffect, useState } from "react" import { getApiKey } from "../api" @@ -16,7 +16,7 @@ const CliAuthenticationPage: React.FC = () => { useEffect(() => { if (me?.id) { - getApiKey().then(({ key }) => { + void getApiKey().then(({ key }) => { setApiKey(key) }) } From 9ec938ec7bd58d4c50f9d05955c44c5be341bed8 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 16 Feb 2022 23:12:31 +0000 Subject: [PATCH 04/17] Add login prompt for non-first-time-user case --- cli/login.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++---- go.mod | 1 + go.sum | 1 + 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/cli/login.go b/cli/login.go index 5910b5846ddcd..538e73fd5dcff 100644 --- a/cli/login.go +++ b/cli/login.go @@ -2,13 +2,17 @@ package cli import ( "fmt" + "io/ioutil" "net/url" + "os/exec" "os/user" + "runtime" "strings" "github.com/fatih/color" "github.com/go-playground/validator/v10" "github.com/manifoldco/promptui" + "github.com/pkg/browser" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -16,6 +20,12 @@ import ( "github.com/coder/coder/codersdk" ) +const ( + goosWindows = "windows" + goosLinux = "linux" + goosDarwin = "darwin" +) + func login() *cobra.Command { return &cobra.Command{ Use: "login ", @@ -116,21 +126,99 @@ func login() *cobra.Command { if err != nil { return xerrors.Errorf("login with password: %w", err) } - config := createConfig(cmd) - err = config.Session().Write(resp.SessionToken) - if err != nil { - return xerrors.Errorf("write session token: %w", err) - } - err = config.URL().Write(serverURL.String()) + + err = saveSessionToken(cmd, client, resp.SessionToken, serverURL) if err != nil { - return xerrors.Errorf("write server url: %w", err) + return xerrors.Errorf("save session token: %w", err) } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(username)) return nil } + authURL := *serverURL + authURL.Path = serverURL.Path + "/cli-auth" + if err := openURL(authURL.String()); err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Open the following in your browser:\n\n\t%s\n\n", authURL.String()) + } else { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String()) + } + + apiKey, err := prompt(cmd, &promptui.Prompt{ + Label: "Paste your token here:", + Validate: func(token string) error { + client.SessionToken = token + _, err := client.User(cmd.Context(), "me") + if err != nil { + return xerrors.New("That's not a valid token!") + } + return err + }, + }) + if err != nil { + return xerrors.Errorf("specify email prompt: %w", err) + } + + err = saveSessionToken(cmd, client, apiKey, serverURL) + if err != nil { + return xerrors.Errorf("save session token after login: %w", err) + } + return nil }, } } + +func saveSessionToken(cmd *cobra.Command, client *codersdk.Client, sessionToken string, serverURL *url.URL) error { + // Login to get user data - verify it is OK before persisting + client.SessionToken = sessionToken + resp, err := client.User(cmd.Context(), "me") + if err != nil { + return xerrors.Errorf("get user: ", err) + } + + config := createConfig(cmd) + err = config.Session().Write(sessionToken) + if err != nil { + return xerrors.Errorf("write session token: %w", err) + } + err = config.URL().Write(serverURL.String()) + if err != nil { + return xerrors.Errorf("write server url: %w", err) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(resp.Username)) + return nil +} + +// isWSL determines if coder-cli is running within Windows Subsystem for Linux +func isWSL() (bool, error) { + if runtime.GOOS == goosDarwin || runtime.GOOS == goosWindows { + return false, nil + } + data, err := ioutil.ReadFile("/proc/version") + if err != nil { + return false, xerrors.Errorf("read /proc/version: %w", err) + } + return strings.Contains(strings.ToLower(string(data)), "microsoft"), nil +} + +// openURL opens the provided URL via user's default browser +func openURL(url string) error { + var cmd string + var args []string + + wsl, err := isWSL() + if err != nil { + return xerrors.Errorf("test running Windows Subsystem for Linux: %w", err) + } + + if wsl { + cmd = "cmd.exe" + args = []string{"/c", "start"} + url = strings.ReplaceAll(url, "&", "^&") + args = append(args, url) + return exec.Command(cmd, args...).Start() + } + + return browser.OpenURL(url) +} diff --git a/go.mod b/go.mod index 290c7d3758b85..3ad5bd3f30687 100644 --- a/go.mod +++ b/go.mod @@ -113,6 +113,7 @@ require ( github.com/pion/stun v0.3.5 // indirect github.com/pion/turn/v2 v2.0.6 // indirect github.com/pion/udp v0.1.1 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect diff --git a/go.sum b/go.sum index 8e4c8c18401ae..5ffc19a3f44ea 100644 --- a/go.sum +++ b/go.sum @@ -1060,6 +1060,7 @@ github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M github.com/pion/webrtc/v3 v3.1.23 h1:suyNiF9o2/6SBsyWA1UweraUWYkaHCNJdt/16b61I5w= github.com/pion/webrtc/v3 v3.1.23/go.mod h1:L5S/oAhL0Fzt/rnftVQRrP80/j5jygY7XRZzWwFx6P4= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 87fc9e2b0cd5624d8d253ee0aaf75682719b2e22 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 00:08:05 +0000 Subject: [PATCH 05/17] Add test case for login --- cli/clitest/clitest_test.go | 5 +-- cli/login_test.go | 61 ++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/cli/clitest/clitest_test.go b/cli/clitest/clitest_test.go index f5be5a45db12c..108addc0d91ce 100644 --- a/cli/clitest/clitest_test.go +++ b/cli/clitest/clitest_test.go @@ -3,11 +3,12 @@ package clitest_test import ( "testing" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/expect" - "github.com/stretchr/testify/require" - "go.uber.org/goleak" ) func TestMain(m *testing.M) { diff --git a/cli/login_test.go b/cli/login_test.go index 43859ba56199c..cac19400632df 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -1,11 +1,13 @@ package cli_test import ( + "context" "testing" "github.com/coder/coder/cli/clitest" - "github.com/coder/coder/expect" + "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/expect" "github.com/stretchr/testify/require" ) @@ -50,4 +52,61 @@ func TestLogin(t *testing.T) { _, err := console.ExpectString("Welcome to Coder") require.NoError(t, err) }) + + t.Run("ExistingUserValidTokenTTY", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ + Username: "test-user", + Email: "test-user@coder.com", + Organization: "acme-corp", + Password: "password", + }) + require.NoError(t, err) + token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: "test-user@coder.com", + Password: "password", + }) + require.NoError(t, err) + + root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty") + console := expect.NewTestConsole(t, root) + go func() { + err := root.Execute() + require.NoError(t, err) + }() + + _, err = console.ExpectString("Paste your token here:") + require.NoError(t, err) + _, err = console.SendLine(token.SessionToken) + require.NoError(t, err) + _, err = console.ExpectString("Welcome to Coder") + require.NoError(t, err) + }) + + t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ + Username: "test-user", + Email: "test-user@coder.com", + Organization: "acme-corp", + Password: "password", + }) + require.NoError(t, err) + + root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty") + console := expect.NewTestConsole(t, root) + go func() { + err := root.Execute() + require.Error(t, err) + }() + + _, err = console.ExpectString("Paste your token here:") + require.NoError(t, err) + _, err = console.SendLine("an-invalid-token") + require.NoError(t, err) + _, err = console.ExpectString("That's not a valid token!") + require.NoError(t, err) + }) } From 6607a5ad55adf9d6c622ef5afb8046e7851409c7 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 00:15:45 +0000 Subject: [PATCH 06/17] Factor out CliAuth token component --- site/components/SignIn/CliAuthToken.tsx | 29 +++++++++++++++++++++++++ site/components/SignIn/index.tsx | 1 + site/pages/cli-auth.tsx | 20 ++++------------- 3 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 site/components/SignIn/CliAuthToken.tsx diff --git a/site/components/SignIn/CliAuthToken.tsx b/site/components/SignIn/CliAuthToken.tsx new file mode 100644 index 0000000000000..a6d0886e60ebf --- /dev/null +++ b/site/components/SignIn/CliAuthToken.tsx @@ -0,0 +1,29 @@ +import Paper from "@material-ui/core/Paper" +import Typography from "@material-ui/core/Typography" +import { makeStyles } from "@material-ui/core/styles" +import React from "react" +import { CodeExample } from "../CodeExample" + +export interface CliAuthTokenProps { + sessionToken: string +} + +export const CliAuthToken: React.FC = ({ sessionToken }) => { + const styles = useStyles() + return ( + + Session Token + + + ) +} + +const useStyles = makeStyles((theme) => ({ + title: { + marginBottom: theme.spacing(2), + }, + container: { + maxWidth: "680px", + padding: theme.spacing(2), + }, +})) diff --git a/site/components/SignIn/index.tsx b/site/components/SignIn/index.tsx index 6ea6f3de7dd2a..066b58003c67c 100644 --- a/site/components/SignIn/index.tsx +++ b/site/components/SignIn/index.tsx @@ -1 +1,2 @@ +export * from "./CliAuthToken" export * from "./SignInForm" diff --git a/site/pages/cli-auth.tsx b/site/pages/cli-auth.tsx index 0df7147d9c4ff..1932c0a4ae8c0 100644 --- a/site/pages/cli-auth.tsx +++ b/site/pages/cli-auth.tsx @@ -1,9 +1,7 @@ -import Paper from "@material-ui/core/Paper" -import Typography from "@material-ui/core/Typography" import { makeStyles } from "@material-ui/core/styles" import React, { useEffect, useState } from "react" import { getApiKey } from "../api" -import { CodeExample } from "../components/CodeExample" +import { CliAuthToken } from "../components/SignIn" import { FullScreenLoader } from "../components/Loader/FullScreenLoader" import { useUser } from "../contexts/UserContext" @@ -28,29 +26,19 @@ const CliAuthenticationPage: React.FC = () => { return (
- - Session Token - - +
) } const useStyles = makeStyles((theme) => ({ root: { - width: "100vh", - height: "100vw", + width: "100vw", + height: "100vh", display: "flex", justifyContent: "center", alignItems: "center", }, - title: { - marginBottom: theme.spacing(2), - }, - container: { - maxWidth: "680px", - padding: theme.spacing(2), - }, })) export default CliAuthenticationPage From 0e323ce2dbd61094635f618fae15449d93155132 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 00:20:02 +0000 Subject: [PATCH 07/17] Add storybook for CliAuthToken --- site/components/SignIn/CliAuthToken.stories.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 site/components/SignIn/CliAuthToken.stories.tsx diff --git a/site/components/SignIn/CliAuthToken.stories.tsx b/site/components/SignIn/CliAuthToken.stories.tsx new file mode 100644 index 0000000000000..b9d8e5baa9a47 --- /dev/null +++ b/site/components/SignIn/CliAuthToken.stories.tsx @@ -0,0 +1,16 @@ +import { Story } from "@storybook/react" +import React from "react" +import { CliAuthToken, CliAuthTokenProps } from "./CliAuthToken" + +export default { + title: "SignIn/CliAuthToken", + component: CliAuthToken, + argTypes: { + sessionToken: { control: "text", defaultValue: "some-session-token" }, + }, +} + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = {} From 9cc7e942388da63aac0cb334c2b830bf8a6384f8 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 00:45:45 +0000 Subject: [PATCH 08/17] Add test case for CliAuthToken --- site/components/SignIn/CliAuthToken.test.tsx | 16 ++++++++++++++++ site/pages/cli-auth.tsx | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 site/components/SignIn/CliAuthToken.test.tsx diff --git a/site/components/SignIn/CliAuthToken.test.tsx b/site/components/SignIn/CliAuthToken.test.tsx new file mode 100644 index 0000000000000..d17fdbcce88ea --- /dev/null +++ b/site/components/SignIn/CliAuthToken.test.tsx @@ -0,0 +1,16 @@ +import React from "react" +import { screen } from "@testing-library/react" +import { render } from "../../test_helpers" + +import { CliAuthToken } from "./CliAuthToken" + +describe("CliAuthToken", () => { + it("renders content", async () => { + // When + render() + + // Then + await screen.findByText("Session Token") + await screen.findByText("test-token") + }) +}) diff --git a/site/pages/cli-auth.tsx b/site/pages/cli-auth.tsx index 1932c0a4ae8c0..a31c682d93090 100644 --- a/site/pages/cli-auth.tsx +++ b/site/pages/cli-auth.tsx @@ -31,7 +31,7 @@ const CliAuthenticationPage: React.FC = () => { ) } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(() => ({ root: { width: "100vw", height: "100vh", From 7774e29341d790d2858c1a08dec9b3fea273bfd7 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 00:56:57 +0000 Subject: [PATCH 09/17] Add codersdk function + test --- coderd/users_test.go | 27 +++++++++++++++++++++++++++ codersdk/users.go | 14 ++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/coderd/users_test.go b/coderd/users_test.go index 8ef1464856f75..0c7af6183eb8a 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -119,6 +119,33 @@ func TestOrganizationsByUser(t *testing.T) { require.Len(t, orgs, 1) } +func TestPostAPIKey(t *testing.T) { + t.Parallel() + t.Run("InvalidUser", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + + // Clear session token + client.SessionToken = "" + // ...and request an API key + _, err := client.CreateApiKey(context.Background()) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + }) + + t.Run("Success", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + apiKey, err := client.CreateApiKey(context.Background()) + require.NotNil(t, apiKey) + require.GreaterOrEqual(t, len(apiKey.Key), 2) + require.NoError(t, err) + }) +} + func TestPostLogin(t *testing.T) { t.Parallel() t.Run("InvalidUser", func(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index b17b5a9931e6e..bf90fb5c1f6a9 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -56,6 +56,20 @@ func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) ( return user, json.NewDecoder(res.Body).Decode(&user) } +// CreateApiKey calls the /api-key API +func (c *Client) CreateApiKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/api-keys"), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode > http.StatusCreated { + return nil, readBodyAsError(res) + } + apiKey := &coderd.GenerateAPIKeyResponse{} + return apiKey, json.NewDecoder(res.Body).Decode(apiKey) +} + // LoginWithPassword creates a session token authenticating with an email and password. // Call `SetSessionToken()` to apply the newly acquired token to the client. func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPasswordRequest) (coderd.LoginWithPasswordResponse, error) { From 8ea29203a78b7b4d2e75b52dd44c0e7ab505bf42 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 01:01:19 +0000 Subject: [PATCH 10/17] Fix lint issues --- cli/login.go | 11 +++++------ cli/login_test.go | 3 ++- cli/workspacecreate_test.go | 3 ++- coderd/coderd.go | 2 +- coderd/projectimport_test.go | 3 ++- coderd/users.go | 6 +++--- coderd/users_test.go | 4 ++-- codersdk/projectimport_test.go | 5 +++-- codersdk/users.go | 6 +++--- 9 files changed, 23 insertions(+), 20 deletions(-) diff --git a/cli/login.go b/cli/login.go index 538e73fd5dcff..4068bb97b8fbe 100644 --- a/cli/login.go +++ b/cli/login.go @@ -22,7 +22,6 @@ import ( const ( goosWindows = "windows" - goosLinux = "linux" goosDarwin = "darwin" ) @@ -173,7 +172,7 @@ func saveSessionToken(cmd *cobra.Command, client *codersdk.Client, sessionToken client.SessionToken = sessionToken resp, err := client.User(cmd.Context(), "me") if err != nil { - return xerrors.Errorf("get user: ", err) + return xerrors.Errorf("get user: %w", err) } config := createConfig(cmd) @@ -203,7 +202,7 @@ func isWSL() (bool, error) { } // openURL opens the provided URL via user's default browser -func openURL(url string) error { +func openURL(urlToOpen string) error { var cmd string var args []string @@ -215,10 +214,10 @@ func openURL(url string) error { if wsl { cmd = "cmd.exe" args = []string{"/c", "start"} - url = strings.ReplaceAll(url, "&", "^&") - args = append(args, url) + urlToOpen = strings.ReplaceAll(urlToOpen, "&", "^&") + args = append(args, urlToOpen) return exec.Command(cmd, args...).Start() } - return browser.OpenURL(url) + return browser.OpenURL(urlToOpen) } diff --git a/cli/login_test.go b/cli/login_test.go index 94647419c4390..eb5063d57b93f 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/console" - "github.com/stretchr/testify/require" ) func TestLogin(t *testing.T) { diff --git a/cli/workspacecreate_test.go b/cli/workspacecreate_test.go index 306caa65c4b0c..7513a49bc2f98 100644 --- a/cli/workspacecreate_test.go +++ b/cli/workspacecreate_test.go @@ -3,12 +3,13 @@ package cli_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/console" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" - "github.com/stretchr/testify/require" ) func TestWorkspaceCreate(t *testing.T) { diff --git a/coderd/coderd.go b/coderd/coderd.go index 6409aa3f55c20..7211c555bccea 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -40,7 +40,7 @@ func New(options *Options) http.Handler { r.Use( httpmw.ExtractAPIKey(options.Database, nil), ) - r.Post("/", api.postApiKey) + r.Post("/", api.postAPIKey) }) // Used for setup. diff --git a/coderd/projectimport_test.go b/coderd/projectimport_test.go index 06140190f51d5..b9df691233576 100644 --- a/coderd/projectimport_test.go +++ b/coderd/projectimport_test.go @@ -5,13 +5,14 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" "github.com/coder/coder/database" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" - "github.com/stretchr/testify/require" ) func TestPostProjectImportByOrganization(t *testing.T) { diff --git a/coderd/users.go b/coderd/users.go index 87a46aeca7366..c4063ae5e69a7 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -318,7 +318,7 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { } // Creates a new API key, used for logging in via the CLI -func (api *api) postApiKey(rw http.ResponseWriter, r *http.Request) { +func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) userID := apiKey.UserID @@ -348,10 +348,10 @@ func (api *api) postApiKey(rw http.ResponseWriter, r *http.Request) { } // This format is consumed by the APIKey middleware. - generatedApiKey := fmt.Sprintf("%s-%s", keyID, keySecret) + generatedAPIKey := fmt.Sprintf("%s-%s", keyID, keySecret) render.Status(r, http.StatusCreated) - render.JSON(rw, r, GenerateAPIKeyResponse{Key: generatedApiKey}) + render.JSON(rw, r, GenerateAPIKeyResponse{Key: generatedAPIKey}) } // Clear the user's session cookie diff --git a/coderd/users_test.go b/coderd/users_test.go index 0c7af6183eb8a..bc95f8a19a922 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -129,7 +129,7 @@ func TestPostAPIKey(t *testing.T) { // Clear session token client.SessionToken = "" // ...and request an API key - _, err := client.CreateApiKey(context.Background()) + _, err := client.CreateAPIKey(context.Background()) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) @@ -139,7 +139,7 @@ func TestPostAPIKey(t *testing.T) { t.Parallel() client := coderdtest.New(t) _ = coderdtest.CreateInitialUser(t, client) - apiKey, err := client.CreateApiKey(context.Background()) + apiKey, err := client.CreateAPIKey(context.Background()) require.NotNil(t, apiKey) require.GreaterOrEqual(t, len(apiKey.Key), 2) require.NoError(t, err) diff --git a/codersdk/projectimport_test.go b/codersdk/projectimport_test.go index 8cc6b28a23f6c..ccbe01345845a 100644 --- a/codersdk/projectimport_test.go +++ b/codersdk/projectimport_test.go @@ -5,12 +5,13 @@ import ( "testing" "time" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" - "github.com/google/uuid" - "github.com/stretchr/testify/require" ) func TestCreateProjectImportJob(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index bf90fb5c1f6a9..3bc66c764266c 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -56,9 +56,9 @@ func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) ( return user, json.NewDecoder(res.Body).Decode(&user) } -// CreateApiKey calls the /api-key API -func (c *Client) CreateApiKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/api-keys"), nil) +// CreateAPIKey calls the /api-key API +func (c *Client) CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse, error) { + res, err := c.request(ctx, http.MethodPost, "/api/v2/api-keys", nil) if err != nil { return nil, err } From 669f2ca433c9536e87f5136b0ff3ff7f21e69526 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 01:57:39 +0000 Subject: [PATCH 11/17] Discard output of browser pkg --- cli/login.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/login.go b/cli/login.go index 4068bb97b8fbe..20dd46d71f234 100644 --- a/cli/login.go +++ b/cli/login.go @@ -219,5 +219,10 @@ func openURL(urlToOpen string) error { return exec.Command(cmd, args...).Start() } + // Hide output from the browser library, + // otherwise we can get really verbose and non-actionable messages + // when in SSH or another type of headless session + browser.Stderr = ioutil.Discard + browser.Stdout = ioutil.Discard return browser.OpenURL(urlToOpen) } From 5ecba6da44254dea53768bad4573c29b015a0687 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 21:46:44 +0000 Subject: [PATCH 12/17] Add mask for session token prompt --- cli/login.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/login.go b/cli/login.go index 2d51a49f8b7fa..55b76e03c81bb 100644 --- a/cli/login.go +++ b/cli/login.go @@ -144,6 +144,7 @@ func login() *cobra.Command { apiKey, err := prompt(cmd, &promptui.Prompt{ Label: "Paste your token here:", + Mask: '*', Validate: func(token string) error { client.SessionToken = token _, err := client.User(cmd.Context(), "me") From 13696a7a663f7edb332615fdf7b57136352b1fba Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 22:09:59 +0000 Subject: [PATCH 13/17] Inline saveSessionToken into both code paths --- cli/login.go | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/cli/login.go b/cli/login.go index 55b76e03c81bb..21d384d74c406 100644 --- a/cli/login.go +++ b/cli/login.go @@ -126,11 +126,18 @@ func login() *cobra.Command { return xerrors.Errorf("login with password: %w", err) } - err = saveSessionToken(cmd, client, resp.SessionToken, serverURL) + sessionToken := resp.SessionToken + config := createConfig(cmd) + err = config.Session().Write(sessionToken) if err != nil { - return xerrors.Errorf("save session token: %w", err) + return xerrors.Errorf("write session token: %w", err) + } + err = config.URL().Write(serverURL.String()) + if err != nil { + return xerrors.Errorf("write server url: %w", err) } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(username)) return nil } @@ -142,7 +149,7 @@ func login() *cobra.Command { _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String()) } - apiKey, err := prompt(cmd, &promptui.Prompt{ + sessionToken, err := prompt(cmd, &promptui.Prompt{ Label: "Paste your token here:", Mask: '*', Validate: func(token string) error { @@ -158,35 +165,31 @@ func login() *cobra.Command { return xerrors.Errorf("paste token prompt: %w", err) } - err = saveSessionToken(cmd, client, apiKey, serverURL) + // Login to get user data - verify it is OK before persisting + client.SessionToken = sessionToken + resp, err := client.User(cmd.Context(), "me") + if err != nil { + return xerrors.Errorf("get user: %w", err) + } + + config := createConfig(cmd) + err = config.Session().Write(sessionToken) + if err != nil { + return xerrors.Errorf("write session token: %w", err) + } + err = config.URL().Write(serverURL.String()) if err != nil { - return xerrors.Errorf("save session token after login: %w", err) + return xerrors.Errorf("write server url: %w", err) } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(resp.Username)) return nil }, } } func saveSessionToken(cmd *cobra.Command, client *codersdk.Client, sessionToken string, serverURL *url.URL) error { - // Login to get user data - verify it is OK before persisting - client.SessionToken = sessionToken - resp, err := client.User(cmd.Context(), "me") - if err != nil { - return xerrors.Errorf("get user: %w", err) - } - - config := createConfig(cmd) - err = config.Session().Write(sessionToken) - if err != nil { - return xerrors.Errorf("write session token: %w", err) - } - err = config.URL().Write(serverURL.String()) - if err != nil { - return xerrors.Errorf("write server url: %w", err) - } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(resp.Username)) return nil } From dccb009ef3fa53c76ca7ac2f6fb3cfb9a0039897 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 22:14:29 +0000 Subject: [PATCH 14/17] Move setting browser.Stderr/Stdout to init --- cli/login.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cli/login.go b/cli/login.go index 21d384d74c406..a0cf765cb8d00 100644 --- a/cli/login.go +++ b/cli/login.go @@ -25,6 +25,16 @@ const ( goosDarwin = "darwin" ) +func init() { + // Hide output from the browser library, + // otherwise we can get really verbose and non-actionable messages + // when in SSH or another type of headless session + // NOTE: This needs to be in `init` to prevent data races + // (multiple threads trying to set the global browser.Std* variables) + browser.Stderr = ioutil.Discard + browser.Stdout = ioutil.Discard +} + func login() *cobra.Command { return &cobra.Command{ Use: "login ", @@ -223,10 +233,5 @@ func openURL(urlToOpen string) error { return exec.Command(cmd, args...).Start() } - // Hide output from the browser library, - // otherwise we can get really verbose and non-actionable messages - // when in SSH or another type of headless session - browser.Stderr = ioutil.Discard - browser.Stdout = ioutil.Discard return browser.OpenURL(urlToOpen) } From ca1a4583889e849f5b5f86bc8dc8a53004e57cfd Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 23:31:36 +0000 Subject: [PATCH 15/17] Change route /api-keys -> /users/{user}/keys --- coderd/coderd.go | 14 +++++--------- coderd/users.go | 15 +++++++++++---- coderd/users_test.go | 2 +- codersdk/users.go | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 7211c555bccea..1213b04aa0a86 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -36,12 +36,6 @@ func New(options *Options) http.Handler { }) r.Post("/login", api.postLogin) r.Post("/logout", api.postLogout) - r.Route("/api-keys", func(r chi.Router) { - r.Use( - httpmw.ExtractAPIKey(options.Database, nil), - ) - r.Post("/", api.postAPIKey) - }) // Used for setup. r.Get("/user", api.user) @@ -51,10 +45,12 @@ func New(options *Options) http.Handler { httpmw.ExtractAPIKey(options.Database, nil), ) r.Post("/", api.postUsers) - r.Group(func(r chi.Router) { + + r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/{user}", api.userByName) - r.Get("/{user}/organizations", api.organizationsByUser) + r.Get("/", api.userByName) + r.Get("/organizations", api.organizationsByUser) + r.Post("/keys", api.postKeyForUser) }) }) r.Route("/projects", func(r chi.Router) { diff --git a/coderd/users.go b/coderd/users.go index c4063ae5e69a7..a660f31fdfa94 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -317,10 +317,17 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { }) } -// Creates a new API key, used for logging in via the CLI -func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) { +// Creates a new session key, used for logging in via the CLI +func (api *api) postKeyForUser(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) apiKey := httpmw.APIKey(r) - userID := apiKey.UserID + + if user.ID != apiKey.UserID { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "Keys can only be generated for the authenticated user", + }) + return + } keyID, keySecret, err := generateAPIKeyIDSecret() if err != nil { @@ -333,7 +340,7 @@ func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) { _, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: keyID, - UserID: userID, + UserID: apiKey.UserID, ExpiresAt: database.Now().AddDate(1, 0, 0), // Expire after 1 year (same as v1) CreatedAt: database.Now(), UpdatedAt: database.Now(), diff --git a/coderd/users_test.go b/coderd/users_test.go index bc95f8a19a922..3a3a5ee8eac76 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -119,7 +119,7 @@ func TestOrganizationsByUser(t *testing.T) { require.Len(t, orgs, 1) } -func TestPostAPIKey(t *testing.T) { +func TestPostKey(t *testing.T) { t.Parallel() t.Run("InvalidUser", func(t *testing.T) { t.Parallel() diff --git a/codersdk/users.go b/codersdk/users.go index 3bc66c764266c..08b152f9cf8d0 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -58,7 +58,7 @@ func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) ( // CreateAPIKey calls the /api-key API func (c *Client) CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/api-keys", nil) + res, err := c.request(ctx, http.MethodPost, "/api/v2/users/me/keys", nil) if err != nil { return nil, err } From b232d0c2c0c9d02b60575eec73e110c32916565e Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 17 Feb 2022 23:32:57 +0000 Subject: [PATCH 16/17] Remove leftover saveSessionToken func --- cli/login.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cli/login.go b/cli/login.go index a0cf765cb8d00..9658edb835c2e 100644 --- a/cli/login.go +++ b/cli/login.go @@ -198,11 +198,6 @@ func login() *cobra.Command { } } -func saveSessionToken(cmd *cobra.Command, client *codersdk.Client, sessionToken string, serverURL *url.URL) error { - - return nil -} - // isWSL determines if coder-cli is running within Windows Subsystem for Linux func isWSL() (bool, error) { if runtime.GOOS == goosDarwin || runtime.GOOS == goosWindows { From 0d829b332840523ce04fcf45712391ddf33c1719 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Fri, 18 Feb 2022 03:41:16 +0000 Subject: [PATCH 17/17] Update route in UI for getting new token --- site/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/api.ts b/site/api.ts index eb37583d262e7..5339ccd15d851 100644 --- a/site/api.ts +++ b/site/api.ts @@ -141,7 +141,7 @@ export const logout = async (): Promise => { } export const getApiKey = async (): Promise<{ key: string }> => { - const response = await fetch("/api/v2/api-keys", { + const response = await fetch("/api/v2/users/me/keys", { method: "POST", }) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy