From 75e26afe66757ba9d5ab7fe560b9647d529a0703 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 5 Oct 2022 20:00:11 +0000 Subject: [PATCH 01/17] feat: machine keys --- coderd/coderd.go | 2 +- coderd/database/dump.sql | 3 +- .../000055_api_key_application.down.sql | 8 +++ .../000055_api_key_application.up.sql | 1 + coderd/database/models.go | 1 + coderd/users.go | 7 +-- coderd/users_test.go | 45 +++++------------ codersdk/api_key.go | 50 +++++++++++++++++++ codersdk/users.go | 39 --------------- 9 files changed, 80 insertions(+), 76 deletions(-) create mode 100644 coderd/database/migrations/000055_api_key_application.down.sql create mode 100644 coderd/database/migrations/000055_api_key_application.up.sql create mode 100644 codersdk/api_key.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 011f29927d92e..f825c0d0c0820 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -399,7 +399,7 @@ func New(options *Options) *API { r.Get("/roles", api.userRoles) r.Route("/keys", func(r chi.Router) { - r.Post("/", api.postAPIKey) + r.Post("/machine", api.postMachineAPIKey) r.Get("/{keyid}", api.apiKey) }) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9413e0822d154..78f55cbc051db 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -33,7 +33,8 @@ CREATE TYPE log_source AS ENUM ( CREATE TYPE login_type AS ENUM ( 'password', 'github', - 'oidc' + 'oidc', + 'machine' ); CREATE TYPE parameter_destination_scheme AS ENUM ( diff --git a/coderd/database/migrations/000055_api_key_application.down.sql b/coderd/database/migrations/000055_api_key_application.down.sql new file mode 100644 index 0000000000000..02069306046ed --- /dev/null +++ b/coderd/database/migrations/000055_api_key_application.down.sql @@ -0,0 +1,8 @@ +CREATE TYPE old_login_type AS ENUM ( + 'password', + 'github', + 'oidc' +); +ALTER TABLE api_keys ALTER COLUMN login_type TYPE old_login_type USING (login_type::text::old_login_type); +DROP TYPE login_type; +ALTER TYPE old_login_type RENAME TO login_type; diff --git a/coderd/database/migrations/000055_api_key_application.up.sql b/coderd/database/migrations/000055_api_key_application.up.sql new file mode 100644 index 0000000000000..3580bdc1b809f --- /dev/null +++ b/coderd/database/migrations/000055_api_key_application.up.sql @@ -0,0 +1 @@ +ALTER TYPE login_type ADD VALUE IF NOT EXISTS 'machine'; diff --git a/coderd/database/models.go b/coderd/database/models.go index bfd7d3f7af1ad..8e7fc54ff8e9b 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -120,6 +120,7 @@ const ( LoginTypePassword LoginType = "password" LoginTypeGithub LoginType = "github" LoginTypeOIDC LoginType = "oidc" + LoginTypeMachine LoginType = "machine" ) func (e *LoginType) Scan(src interface{}) error { diff --git a/coderd/users.go b/coderd/users.go index 17c788f72b8f2..8cee70f8e171e 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -943,8 +943,8 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) { }) } -// Creates a new session key, used for logging in via the CLI. -func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) { +// Creates a new machine session key that effectively doesn't expire. +func (api *API) postMachineAPIKey(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() user := httpmw.UserParam(r) @@ -953,7 +953,8 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) { return } - lifeTime := time.Hour * 24 * 7 + // machine keys last 100 years + lifeTime := time.Hour * 876000 cookie, err := api.createAPIKey(ctx, createAPIKeyParams{ UserID: user.ID, LoginType: database.LoginTypePassword, diff --git a/coderd/users_test.go b/coderd/users_test.go index e6c8d5da26e17..9d45479831da7 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -285,15 +285,14 @@ func TestPostLogin(t *testing.T) { require.NoError(t, err, "fetch login key") require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400") - // Generated tokens have a longer life - token, err := client.CreateAPIKey(ctx, admin.UserID.String()) - require.NoError(t, err, "make new api key") + // Machine tokens have a longer life + token, err := client.CreateMachineKey(ctx) + require.NoError(t, err, "make new machine api key") split = strings.Split(token.Key, "-") apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0]) require.NoError(t, err, "fetch api key") - require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*6)), "api key lasts more than 6 days") - require.True(t, apiKey.ExpiresAt.After(key.ExpiresAt.Add(time.Hour)), "api key should be longer expires") + require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*438300)), "api key lasts more than 50 years") require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "api key should have longer lifetime") }) } @@ -1195,36 +1194,18 @@ func TestGetUsers(t *testing.T) { }) } -func TestPostAPIKey(t *testing.T) { +func TestPostMachineKey(t *testing.T) { t.Parallel() - t.Run("InvalidUser", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - client.SessionToken = "" - _, err := client.CreateAPIKey(ctx, codersdk.Me) - 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, nil) - _ = coderdtest.CreateFirstUser(t, client) + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - apiKey, err := client.CreateAPIKey(ctx, codersdk.Me) - require.NotNil(t, apiKey) - require.GreaterOrEqual(t, len(apiKey.Key), 2) - require.NoError(t, err) - }) + apiKey, err := client.CreateMachineKey(ctx) + require.NotNil(t, apiKey) + require.GreaterOrEqual(t, len(apiKey.Key), 2) + require.NoError(t, err) } func TestWorkspacesByUser(t *testing.T) { diff --git a/codersdk/api_key.go b/codersdk/api_key.go new file mode 100644 index 0000000000000..6c83c69876634 --- /dev/null +++ b/codersdk/api_key.go @@ -0,0 +1,50 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/google/uuid" +) + +type APIKey struct { + ID string `json:"id" validate:"required"` + // NOTE: do not ever return the HashedSecret + UserID uuid.UUID `json:"user_id" validate:"required"` + LastUsed time.Time `json:"last_used" validate:"required"` + ExpiresAt time.Time `json:"expires_at" validate:"required"` + CreatedAt time.Time `json:"created_at" validate:"required"` + UpdatedAt time.Time `json:"updated_at" validate:"required"` + LoginType LoginType `json:"login_type" validate:"required"` + LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"` +} + +// CreateMachineKey generates an API key that doesn't expire. +func (c *Client) CreateMachineKey(ctx context.Context) (*GenerateAPIKeyResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/machine", Me), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode > http.StatusCreated { + return nil, readBodyAsError(res) + } + apiKey := &GenerateAPIKeyResponse{} + return apiKey, json.NewDecoder(res.Body).Decode(apiKey) +} + +func (c *Client) GetAPIKey(ctx context.Context, user string, id string) (*APIKey, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", user, id), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode > http.StatusCreated { + return nil, readBodyAsError(res) + } + apiKey := &APIKey{} + return apiKey, json.NewDecoder(res.Body).Decode(apiKey) +} diff --git a/codersdk/users.go b/codersdk/users.go index 4fce8a3a47bfe..5c5d4157c7544 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -55,18 +55,6 @@ type User struct { AvatarURL string `json:"avatar_url"` } -type APIKey struct { - ID string `json:"id" validate:"required"` - // NOTE: do not ever return the HashedSecret - UserID uuid.UUID `json:"user_id" validate:"required"` - LastUsed time.Time `json:"last_used" validate:"required"` - ExpiresAt time.Time `json:"expires_at" validate:"required"` - CreatedAt time.Time `json:"created_at" validate:"required"` - UpdatedAt time.Time `json:"updated_at" validate:"required"` - LoginType LoginType `json:"login_type" validate:"required"` - LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"` -} - type CreateFirstUserRequest struct { Email string `json:"email" validate:"required,email"` Username string `json:"username" validate:"required,username"` @@ -287,33 +275,6 @@ func (c *Client) GetUserRoles(ctx context.Context, user string) (UserRoles, erro return roles, json.NewDecoder(res.Body).Decode(&roles) } -// CreateAPIKey generates an API key for the user ID provided. -func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKeyResponse, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode > http.StatusCreated { - return nil, readBodyAsError(res) - } - apiKey := &GenerateAPIKeyResponse{} - return apiKey, json.NewDecoder(res.Body).Decode(apiKey) -} - -func (c *Client) GetAPIKey(ctx context.Context, user string, id string) (*APIKey, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", user, id), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode > http.StatusCreated { - return nil, readBodyAsError(res) - } - apiKey := &APIKey{} - 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 LoginWithPasswordRequest) (LoginWithPasswordResponse, error) { From 2689c10c9808c6014d7cd9475a54335ec5188457 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 5 Oct 2022 20:31:16 +0000 Subject: [PATCH 02/17] fix migration conflict --- ...y_application.down.sql => 000056_api_key_application.down.sql} | 0 ...i_key_application.up.sql => 000056_api_key_application.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000055_api_key_application.down.sql => 000056_api_key_application.down.sql} (100%) rename coderd/database/migrations/{000055_api_key_application.up.sql => 000056_api_key_application.up.sql} (100%) diff --git a/coderd/database/migrations/000055_api_key_application.down.sql b/coderd/database/migrations/000056_api_key_application.down.sql similarity index 100% rename from coderd/database/migrations/000055_api_key_application.down.sql rename to coderd/database/migrations/000056_api_key_application.down.sql diff --git a/coderd/database/migrations/000055_api_key_application.up.sql b/coderd/database/migrations/000056_api_key_application.up.sql similarity index 100% rename from coderd/database/migrations/000055_api_key_application.up.sql rename to coderd/database/migrations/000056_api_key_application.up.sql From d19d8641618b738a2b2df2a9dc555f3752961008 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 5 Oct 2022 20:32:40 +0000 Subject: [PATCH 03/17] make gen --- codersdk/api_key.go | 9 +++++++++ codersdk/users.go | 8 -------- site/src/api/typesGenerated.ts | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/codersdk/api_key.go b/codersdk/api_key.go index 6c83c69876634..ee9d3bb6ba6c9 100644 --- a/codersdk/api_key.go +++ b/codersdk/api_key.go @@ -22,6 +22,15 @@ type APIKey struct { LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"` } +type LoginType string + +const ( + LoginTypePassword LoginType = "password" + LoginTypeGithub LoginType = "github" + LoginTypeOIDC LoginType = "oidc" + LoginTypeMachine LoginType = "machine" +) + // CreateMachineKey generates an API key that doesn't expire. func (c *Client) CreateMachineKey(ctx context.Context) (*GenerateAPIKeyResponse, error) { res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/machine", Me), nil) diff --git a/codersdk/users.go b/codersdk/users.go index 5c5d4157c7544..a37e41d84ec5a 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -22,14 +22,6 @@ const ( UserStatusSuspended UserStatus = "suspended" ) -type LoginType string - -const ( - LoginTypePassword LoginType = "password" - LoginTypeGithub LoginType = "github" - LoginTypeOIDC LoginType = "oidc" -) - type UsersRequest struct { Search string `json:"search,omitempty" typescript:"-"` // Filter users by status. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5bfe3b788348e..5322135001b1a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1,6 +1,6 @@ // Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT. -// From codersdk/users.go +// From codersdk/api_key.go export interface APIKey { readonly id: string readonly user_id: string @@ -685,8 +685,8 @@ export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" // From codersdk/provisionerdaemons.go export type LogSource = "provisioner" | "provisioner_daemon" -// From codersdk/users.go -export type LoginType = "github" | "oidc" | "password" +// From codersdk/api_key.go +export type LoginType = "github" | "machine" | "oidc" | "password" // From codersdk/parameters.go export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable" From cace329c41be0dbebda2cea04437fded9f37f040 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 5 Oct 2022 20:33:06 +0000 Subject: [PATCH 04/17] fmt --- coderd/database/migrations/000056_api_key_application.down.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/migrations/000056_api_key_application.down.sql b/coderd/database/migrations/000056_api_key_application.down.sql index 02069306046ed..30c8ca34d0e1f 100644 --- a/coderd/database/migrations/000056_api_key_application.down.sql +++ b/coderd/database/migrations/000056_api_key_application.down.sql @@ -1,7 +1,7 @@ CREATE TYPE old_login_type AS ENUM ( 'password', 'github', - 'oidc' + 'oidc' ); ALTER TABLE api_keys ALTER COLUMN login_type TYPE old_login_type USING (login_type::text::old_login_type); DROP TYPE login_type; From fd3617b8a2fd062b33ca8fc277061bc951c9d222 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 5 Oct 2022 20:59:26 +0000 Subject: [PATCH 05/17] fix down migration --- .../migrations/000056_api_key_application.down.sql | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/coderd/database/migrations/000056_api_key_application.down.sql b/coderd/database/migrations/000056_api_key_application.down.sql index 30c8ca34d0e1f..0d2e97aef130e 100644 --- a/coderd/database/migrations/000056_api_key_application.down.sql +++ b/coderd/database/migrations/000056_api_key_application.down.sql @@ -1,8 +1,2 @@ -CREATE TYPE old_login_type AS ENUM ( - 'password', - 'github', - 'oidc' -); -ALTER TABLE api_keys ALTER COLUMN login_type TYPE old_login_type USING (login_type::text::old_login_type); -DROP TYPE login_type; -ALTER TYPE old_login_type RENAME TO login_type; +-- You cannot safely remove values from enums https://www.postgresql.org/docs/current/datatype-enum.html +-- You cannot create a new type and do a rename because objects depend on this type now. From e4f498e0b7b92d61719760d8f09432a05e5dd8e8 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 15:40:29 +0000 Subject: [PATCH 06/17] add cli commands --- cli/root.go | 1 + coderd/users_test.go | 4 ++-- codersdk/api_key.go | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/cli/root.go b/cli/root.go index ee2d67d788e64..e90c1e174522e 100644 --- a/cli/root.go +++ b/cli/root.go @@ -93,6 +93,7 @@ func Core() []*cobra.Command { users(), versionCmd(), workspaceAgent(), + keys(), } } diff --git a/coderd/users_test.go b/coderd/users_test.go index 9d45479831da7..42f95509b01a8 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -286,7 +286,7 @@ func TestPostLogin(t *testing.T) { require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400") // Machine tokens have a longer life - token, err := client.CreateMachineKey(ctx) + token, err := client.CreateMachineKey(ctx, codersdk.Me) require.NoError(t, err, "make new machine api key") split = strings.Split(token.Key, "-") apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0]) @@ -1202,7 +1202,7 @@ func TestPostMachineKey(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - apiKey, err := client.CreateMachineKey(ctx) + apiKey, err := client.CreateMachineKey(ctx, codersdk.Me) require.NotNil(t, apiKey) require.GreaterOrEqual(t, len(apiKey.Key), 2) require.NoError(t, err) diff --git a/codersdk/api_key.go b/codersdk/api_key.go index ee9d3bb6ba6c9..0bbf58a473205 100644 --- a/codersdk/api_key.go +++ b/codersdk/api_key.go @@ -32,8 +32,8 @@ const ( ) // CreateMachineKey generates an API key that doesn't expire. -func (c *Client) CreateMachineKey(ctx context.Context) (*GenerateAPIKeyResponse, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/machine", Me), nil) +func (c *Client) CreateMachineKey(ctx context.Context, userID string) (*GenerateAPIKeyResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/machine", userID), nil) if err != nil { return nil, err } @@ -45,8 +45,23 @@ func (c *Client) CreateMachineKey(ctx context.Context) (*GenerateAPIKeyResponse, return apiKey, json.NewDecoder(res.Body).Decode(apiKey) } -func (c *Client) GetAPIKey(ctx context.Context, user string, id string) (*APIKey, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", user, id), nil) +// ListMachineKeys list machine API keys. +func (c *Client) ListMachineKeys(ctx context.Context, userID string) ([]APIKey, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/machine", userID), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode > http.StatusOK { + return nil, readBodyAsError(res) + } + var apiKey = []APIKey{} + return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) +} + +// GetAPIKey returns the api key by id. +func (c *Client) GetAPIKey(ctx context.Context, userID string, id string) (*APIKey, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil) if err != nil { return nil, err } @@ -57,3 +72,16 @@ func (c *Client) GetAPIKey(ctx context.Context, user string, id string) (*APIKey apiKey := &APIKey{} return apiKey, json.NewDecoder(res.Body).Decode(apiKey) } + +// DeleteAPIKey deletes API key by id. +func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode > http.StatusNoContent { + return readBodyAsError(res) + } + return nil +} From 7e299d79546ee440f4ee94c9ea2df9b0ae8ee821 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 15:40:48 +0000 Subject: [PATCH 07/17] add cli commands --- cli/keys.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 cli/keys.go diff --git a/cli/keys.go b/cli/keys.go new file mode 100644 index 0000000000000..43484f4f9766d --- /dev/null +++ b/cli/keys.go @@ -0,0 +1,149 @@ +package cli + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/codersdk" +) + +func keys() *cobra.Command { + cmd := &cobra.Command{ + Use: "keys", + Short: "Manage machine keys", + Long: "Machine keys are used to authenticate automated clients to Coder.", + Aliases: []string{"key"}, + Example: formatExamples( + example{ + Description: "Create a machine key for CI/CD scripts", + Command: "coder keys create", + }, + example{ + Description: "List your machine keys", + Command: "coder keys ls", + }, + example{ + Description: "Remove a key by ID", + Command: "coder keys rm WuoWs4ZsMX", + }, + ), + } + cmd.AddCommand( + createKey(), + listKeys(), + removeKey(), + ) + + return cmd +} + +func createKey() *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a machine key", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := CreateClient(cmd) + if err != nil { + return xerrors.Errorf("create codersdk client: %w", err) + } + + res, err := client.CreateMachineKey(cmd.Context(), codersdk.Me) + if err != nil { + return xerrors.Errorf("create machine key: %w", err) + } + + cmd.Println(cliui.Styles.Wrap.Render( + "This is your API key for authenticating to Coder in automated services. 🪄", + )) + cmd.Println() + cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key))) + cmd.Println() + cmd.Println(cliui.Styles.Wrap.Render( + "You can use this API key by setting --%s CLI flag, the %s environment variable, or the %s HTTP header.", + )) + + return nil + }, + } + + return cmd +} + +type keyRow struct { + ID string `table:"id"` + LastUsed time.Time `json:"last_used"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` +} + +func listKeys() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List machine keys", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := CreateClient(cmd) + if err != nil { + return xerrors.Errorf("create codersdk client: %w", err) + } + + keys, err := client.ListMachineKeys(cmd.Context(), codersdk.Me) + if err != nil { + return xerrors.Errorf("create machine key: %w", err) + } + + var rows []keyRow + for _, key := range keys { + rows = append(rows, keyRow{ + ID: key.ID, + LastUsed: key.LastUsed, + ExpiresAt: key.ExpiresAt, + CreatedAt: key.CreatedAt, + }) + } + + out, err := cliui.DisplayTable(rows, "", nil) + if err != nil { + return err + } + + _, err = fmt.Fprintln(cmd.OutOrStdout(), out) + return err + }, + } + + return cmd +} + +func removeKey() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove [id]", + Aliases: []string{"rm"}, + Short: "Delete a machine key", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := CreateClient(cmd) + if err != nil { + return xerrors.Errorf("create codersdk client: %w", err) + } + + err = client.DeleteAPIKey(cmd.Context(), codersdk.Me, args[0]) + if err != nil { + return xerrors.Errorf("delete api key: %w", err) + } + + cmd.Println(cliui.Styles.Wrap.Render( + "API key has been deleted.", + )) + + return nil + }, + } + + return cmd +} From a097819f14797c23fcd6db7ba0d2f280b8749f55 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 16:49:15 +0000 Subject: [PATCH 08/17] add tests --- cli/cliui/table.go | 4 +- cli/keys.go | 20 +- cli/keys_test.go | 66 +++++ coderd/apikey.go | 239 +++++++++++++++++++ coderd/apikey_test.go | 41 ++++ coderd/coderd.go | 10 +- coderd/database/databasefake/databasefake.go | 13 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 39 +++ coderd/database/queries/apikeys.sql | 3 + coderd/users.go | 162 ------------- codersdk/{api_key.go => apikey.go} | 4 +- site/src/api/typesGenerated.ts | 4 +- 13 files changed, 429 insertions(+), 177 deletions(-) create mode 100644 cli/keys_test.go create mode 100644 coderd/apikey.go create mode 100644 coderd/apikey_test.go rename codersdk/{api_key.go => apikey.go} (95%) diff --git a/cli/cliui/table.go b/cli/cliui/table.go index b4c97d49ce788..cd26c4318246e 100644 --- a/cli/cliui/table.go +++ b/cli/cliui/table.go @@ -153,10 +153,10 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error) // Special type formatting. switch val := v.(type) { case time.Time: - v = val.Format(time.Stamp) + v = val.Format(time.RFC3339) case *time.Time: if val != nil { - v = val.Format(time.Stamp) + v = val.Format(time.RFC3339) } case fmt.Stringer: if val != nil { diff --git a/cli/keys.go b/cli/keys.go index 43484f4f9766d..974623c6a1370 100644 --- a/cli/keys.go +++ b/cli/keys.go @@ -58,13 +58,13 @@ func createKey() *cobra.Command { } cmd.Println(cliui.Styles.Wrap.Render( - "This is your API key for authenticating to Coder in automated services. 🪄", + "Here is your API key. 🪄", )) cmd.Println() cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key))) cmd.Println() cmd.Println(cliui.Styles.Wrap.Render( - "You can use this API key by setting --%s CLI flag, the %s environment variable, or the %s HTTP header.", + fmt.Sprintf("You can use this API key by setting --%s CLI flag, the %s environment variable, or the \"%s\" HTTP header.", varToken, envSessionToken, codersdk.SessionTokenKey), )) return nil @@ -75,10 +75,10 @@ func createKey() *cobra.Command { } type keyRow struct { - ID string `table:"id"` - LastUsed time.Time `json:"last_used"` - ExpiresAt time.Time `json:"expires_at"` - CreatedAt time.Time `json:"created_at"` + ID string `table:"ID"` + LastUsed time.Time `table:"Last Used"` + ExpiresAt time.Time `table:"Expires At"` + CreatedAt time.Time `table:"Created At"` } func listKeys() *cobra.Command { @@ -92,11 +92,17 @@ func listKeys() *cobra.Command { return xerrors.Errorf("create codersdk client: %w", err) } - keys, err := client.ListMachineKeys(cmd.Context(), codersdk.Me) + keys, err := client.GetMachineKeys(cmd.Context(), codersdk.Me) if err != nil { return xerrors.Errorf("create machine key: %w", err) } + if len(keys) == 0 { + cmd.Println(cliui.Styles.Wrap.Render( + "No machine keys found.", + )) + } + var rows []keyRow for _, key := range keys { rows = append(rows, keyRow{ diff --git a/cli/keys_test.go b/cli/keys_test.go new file mode 100644 index 0000000000000..8b554f5473ffd --- /dev/null +++ b/cli/keys_test.go @@ -0,0 +1,66 @@ +package cli_test + +import ( + "bytes" + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/coderd/coderdtest" +) + +func TestMachineKeys(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + // helpful empty response + cmd, root := clitest.New(t, "keys", "ls") + clitest.SetupConfig(t, client, root) + buf := new(bytes.Buffer) + cmd.SetOut(buf) + err := cmd.Execute() + require.NoError(t, err) + res := buf.String() + require.Contains(t, res, "keys found") + + cmd, root = clitest.New(t, "keys", "create") + clitest.SetupConfig(t, client, root) + buf = new(bytes.Buffer) + cmd.SetOut(buf) + err = cmd.Execute() + require.NoError(t, err) + res = buf.String() + require.NotEmpty(t, res) + // find API key in format "XXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXX" + r := regexp.MustCompile("[a-zA-Z0-9]{10}-[a-zA-Z0-9]{22}") + require.Regexp(t, r, res) + key := r.FindString(res) + id := key[:10] + + cmd, root = clitest.New(t, "keys", "ls") + clitest.SetupConfig(t, client, root) + buf = new(bytes.Buffer) + cmd.SetOut(buf) + err = cmd.Execute() + require.NoError(t, err) + res = buf.String() + require.NotEmpty(t, res) + require.Contains(t, res, "ID") + require.Contains(t, res, "EXPIRES AT") + require.Contains(t, res, "CREATED AT") + require.Contains(t, res, "LAST USED") + require.Contains(t, res, id) + + cmd, root = clitest.New(t, "keys", "rm", id) + clitest.SetupConfig(t, client, root) + buf = new(bytes.Buffer) + cmd.SetOut(buf) + err = cmd.Execute() + require.NoError(t, err) + res = buf.String() + require.NotEmpty(t, res) + require.Contains(t, res, "deleted") +} diff --git a/coderd/apikey.go b/coderd/apikey.go new file mode 100644 index 0000000000000..78833e5a248c5 --- /dev/null +++ b/coderd/apikey.go @@ -0,0 +1,239 @@ +package coderd + +import ( + "context" + "crypto/sha256" + "database/sql" + "errors" + "fmt" + "net" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/tabbed/pqtype" + "golang.org/x/xerrors" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/coderd/telemetry" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/cryptorand" +) + +// Creates a new machine session key that effectively doesn't expire. +func (api *API) postMachineAPIKey(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + user := httpmw.UserParam(r) + + if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { + httpapi.ResourceNotFound(rw) + return + } + + // machine keys last 100 years + lifeTime := time.Hour * 876000 + cookie, err := api.createAPIKey(ctx, createAPIKeyParams{ + UserID: user.ID, + LoginType: database.LoginTypeMachine, + ExpiresAt: database.Now().Add(lifeTime), + LifetimeSeconds: int64(lifeTime.Seconds()), + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to create API key.", + Detail: err.Error(), + }) + return + } + + // We intentionally do not set the cookie on the response here. + // Setting the cookie will couple the browser sesion to the API + // key we return here, meaning logging out of the website would + // invalid your CLI key. + httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value}) +} + +func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { + httpapi.ResourceNotFound(rw) + return + } + + keyID := chi.URLParam(r, "keyid") + key, err := api.Database.GetAPIKeyByID(ctx, keyID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching API key.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key)) +} + +func (api *API) machineKeys(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { + httpapi.ResourceNotFound(rw) + return + } + + keys, err := api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeMachine) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusOK, []codersdk.APIKey{}) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching API keys.", + Detail: err.Error(), + }) + return + } + + var apiKeys []codersdk.APIKey + for _, key := range keys { + apiKeys = append(apiKeys, convertAPIKey(key)) + } + + httpapi.Write(ctx, rw, http.StatusOK, apiKeys) +} + +func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + if !api.Authorize(r, rbac.ActionDelete, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { + httpapi.ResourceNotFound(rw) + return + } + + keyID := chi.URLParam(r, "keyid") + err := api.Database.DeleteAPIKeyByID(ctx, keyID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error deleting API key.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusNoContent, nil) +} + +// Generates a new ID and secret for an API key. +func generateAPIKeyIDSecret() (id string, secret string, err error) { + // Length of an API Key ID. + id, err = cryptorand.String(10) + if err != nil { + return "", "", err + } + // Length of an API Key secret. + secret, err = cryptorand.String(22) + if err != nil { + return "", "", err + } + return id, secret, nil +} + +type createAPIKeyParams struct { + UserID uuid.UUID + RemoteAddr string + LoginType database.LoginType + + // Optional. + ExpiresAt time.Time + LifetimeSeconds int64 + Scope database.APIKeyScope +} + +func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) { + keyID, keySecret, err := generateAPIKeyIDSecret() + if err != nil { + return nil, xerrors.Errorf("generate API key: %w", err) + } + hashed := sha256.Sum256([]byte(keySecret)) + + // Default expires at to now+lifetime, or just 24hrs if not set + if params.ExpiresAt.IsZero() { + if params.LifetimeSeconds != 0 { + params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second) + } else { + params.ExpiresAt = database.Now().Add(24 * time.Hour) + } + } + + host, _, _ := net.SplitHostPort(params.RemoteAddr) + ip := net.ParseIP(host) + if ip == nil { + ip = net.IPv4(0, 0, 0, 0) + } + bitlen := len(ip) * 8 + + scope := database.APIKeyScopeAll + if params.Scope != "" { + scope = params.Scope + } + + key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{ + ID: keyID, + UserID: params.UserID, + LifetimeSeconds: params.LifetimeSeconds, + IPAddress: pqtype.Inet{ + IPNet: net.IPNet{ + IP: ip, + Mask: net.CIDRMask(bitlen, bitlen), + }, + Valid: true, + }, + // Make sure in UTC time for common time zone + ExpiresAt: params.ExpiresAt.UTC(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + HashedSecret: hashed[:], + LoginType: params.LoginType, + Scope: scope, + }) + if err != nil { + return nil, xerrors.Errorf("insert API key: %w", err) + } + + api.Telemetry.Report(&telemetry.Snapshot{ + APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)}, + }) + + // This format is consumed by the APIKey middleware. + sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret) + return &http.Cookie{ + Name: codersdk.SessionTokenKey, + Value: sessionToken, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Secure: api.SecureAuthCookie, + }, nil +} diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go new file mode 100644 index 0000000000000..181a5fef665a8 --- /dev/null +++ b/coderd/apikey_test.go @@ -0,0 +1,41 @@ +package coderd_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/testutil" +) + +func TestMachineKeys(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + _ = coderdtest.CreateFirstUser(t, client) + keys, err := client.GetMachineKeys(ctx, codersdk.Me) + require.NoError(t, err) + require.Empty(t, keys) + + res, err := client.CreateMachineKey(ctx, codersdk.Me) + require.NoError(t, err) + require.Greater(t, len(res.Key), 2) + + keys, err = client.GetMachineKeys(ctx, codersdk.Me) + require.NoError(t, err) + require.EqualValues(t, len(keys), 1) + require.Contains(t, res.Key, keys[0].ID) + // expires_at must be greater than 50 years + require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300)) + + err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID) + require.NoError(t, err) + keys, err = client.GetMachineKeys(ctx, codersdk.Me) + require.NoError(t, err) + require.Empty(t, keys) +} diff --git a/coderd/coderd.go b/coderd/coderd.go index f825c0d0c0820..786d4737be2c7 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -399,8 +399,14 @@ func New(options *Options) *API { r.Get("/roles", api.userRoles) r.Route("/keys", func(r chi.Router) { - r.Post("/machine", api.postMachineAPIKey) - r.Get("/{keyid}", api.apiKey) + r.Route("/machine", func(r chi.Router) { + r.Post("/", api.postMachineAPIKey) + r.Get("/", api.machineKeys) + }) + r.Route("/{keyid}", func(r chi.Router) { + r.Get("/", api.apiKey) + r.Delete("/", api.deleteAPIKey) + }) }) r.Route("/organizations", func(r chi.Router) { diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 8713562e2e5e7..bdc26fc53fac2 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -279,6 +279,19 @@ func (q *fakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time return apiKeys, nil } +func (q *fakeQuerier) GetAPIKeysByLoginType(_ context.Context, t database.LoginType) ([]database.APIKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + apiKeys := make([]database.APIKey, 0) + for _, key := range q.apiKeys { + if key.LoginType == t { + apiKeys = append(apiKeys, key) + } + } + return apiKeys, nil +} + func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 56c5f677115d3..c802485666edc 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -25,6 +25,7 @@ type querier interface { DeleteOldAgentStats(ctx context.Context) error DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) + GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) GetActiveUserCount(ctx context.Context) (int64, error) GetAuditLogCount(ctx context.Context, arg GetAuditLogCountParams) (int64, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 66a08f1ccfe66..6795f916c27a1 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -175,6 +175,45 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro return i, err } +const getAPIKeysByLoginType = `-- name: GetAPIKeysByLoginType :many +SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE login_type = $1 +` + +func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) { + rows, err := q.db.QueryContext(ctx, getAPIKeysByLoginType, loginType) + if err != nil { + return nil, err + } + defer rows.Close() + var items []APIKey + for rows.Next() { + var i APIKey + if err := rows.Scan( + &i.ID, + &i.HashedSecret, + &i.UserID, + &i.LastUsed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.LoginType, + &i.LifetimeSeconds, + &i.IPAddress, + &i.Scope, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE last_used > $1 ` diff --git a/coderd/database/queries/apikeys.sql b/coderd/database/queries/apikeys.sql index 7ee97b3beaa79..4dfb39b30b3ed 100644 --- a/coderd/database/queries/apikeys.sql +++ b/coderd/database/queries/apikeys.sql @@ -11,6 +11,9 @@ LIMIT -- name: GetAPIKeysLastUsedAfter :many SELECT * FROM api_keys WHERE last_used > $1; +-- name: GetAPIKeysByLoginType :many +SELECT * FROM api_keys WHERE login_type = $1; + -- name: InsertAPIKey :one INSERT INTO api_keys ( diff --git a/coderd/users.go b/coderd/users.go index 8cee70f8e171e..286cbf7a44ebb 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -3,21 +3,17 @@ package coderd import ( "bytes" "context" - "crypto/sha256" "database/sql" "errors" "fmt" - "net" "net/http" "net/url" "os" "strings" - "time" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" - "github.com/tabbed/pqtype" "golang.org/x/xerrors" "cdr.dev/slog" @@ -31,7 +27,6 @@ import ( "github.com/coder/coder/coderd/userpassword" "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/codersdk" - "github.com/coder/coder/cryptorand" "github.com/coder/coder/examples" ) @@ -943,70 +938,6 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) { }) } -// Creates a new machine session key that effectively doesn't expire. -func (api *API) postMachineAPIKey(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - user := httpmw.UserParam(r) - - if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { - httpapi.ResourceNotFound(rw) - return - } - - // machine keys last 100 years - lifeTime := time.Hour * 876000 - cookie, err := api.createAPIKey(ctx, createAPIKeyParams{ - UserID: user.ID, - LoginType: database.LoginTypePassword, - RemoteAddr: r.RemoteAddr, - // All api generated keys will last 1 week. Browser login tokens have - // a shorter life. - ExpiresAt: database.Now().Add(lifeTime), - LifetimeSeconds: int64(lifeTime.Seconds()), - }) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create API key.", - Detail: err.Error(), - }) - return - } - - // We intentionally do not set the cookie on the response here. - // Setting the cookie will couple the browser sesion to the API - // key we return here, meaning logging out of the website would - // invalid your CLI key. - httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value}) -} - -func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - user = httpmw.UserParam(r) - ) - - if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { - httpapi.ResourceNotFound(rw) - return - } - - keyID := chi.URLParam(r, "keyid") - key, err := api.Database.GetAPIKeyByID(ctx, keyID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.ResourceNotFound(rw) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching API key.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key)) -} - // Clear the user's session cookie. func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -1079,99 +1010,6 @@ func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) { }) } -// Generates a new ID and secret for an API key. -func generateAPIKeyIDSecret() (id string, secret string, err error) { - // Length of an API Key ID. - id, err = cryptorand.String(10) - if err != nil { - return "", "", err - } - // Length of an API Key secret. - secret, err = cryptorand.String(22) - if err != nil { - return "", "", err - } - return id, secret, nil -} - -type createAPIKeyParams struct { - UserID uuid.UUID - RemoteAddr string - LoginType database.LoginType - - // Optional. - ExpiresAt time.Time - LifetimeSeconds int64 - Scope database.APIKeyScope -} - -func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) { - keyID, keySecret, err := generateAPIKeyIDSecret() - if err != nil { - return nil, xerrors.Errorf("generate API key: %w", err) - } - hashed := sha256.Sum256([]byte(keySecret)) - - // Default expires at to now+lifetime, or just 24hrs if not set - if params.ExpiresAt.IsZero() { - if params.LifetimeSeconds != 0 { - params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second) - } else { - params.ExpiresAt = database.Now().Add(24 * time.Hour) - } - } - - host, _, _ := net.SplitHostPort(params.RemoteAddr) - ip := net.ParseIP(host) - if ip == nil { - ip = net.IPv4(0, 0, 0, 0) - } - bitlen := len(ip) * 8 - - scope := database.APIKeyScopeAll - if params.Scope != "" { - scope = params.Scope - } - - key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{ - ID: keyID, - UserID: params.UserID, - LifetimeSeconds: params.LifetimeSeconds, - IPAddress: pqtype.Inet{ - IPNet: net.IPNet{ - IP: ip, - Mask: net.CIDRMask(bitlen, bitlen), - }, - Valid: true, - }, - // Make sure in UTC time for common time zone - ExpiresAt: params.ExpiresAt.UTC(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - HashedSecret: hashed[:], - LoginType: params.LoginType, - Scope: scope, - }) - if err != nil { - return nil, xerrors.Errorf("insert API key: %w", err) - } - - api.Telemetry.Report(&telemetry.Snapshot{ - APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)}, - }) - - // This format is consumed by the APIKey middleware. - sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret) - return &http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: sessionToken, - Path: "/", - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - Secure: api.SecureAuthCookie, - }, nil -} - type CreateUserRequest struct { codersdk.CreateUserRequest LoginType database.LoginType diff --git a/codersdk/api_key.go b/codersdk/apikey.go similarity index 95% rename from codersdk/api_key.go rename to codersdk/apikey.go index 0bbf58a473205..8eee3ca223dcf 100644 --- a/codersdk/api_key.go +++ b/codersdk/apikey.go @@ -45,8 +45,8 @@ func (c *Client) CreateMachineKey(ctx context.Context, userID string) (*Generate return apiKey, json.NewDecoder(res.Body).Decode(apiKey) } -// ListMachineKeys list machine API keys. -func (c *Client) ListMachineKeys(ctx context.Context, userID string) ([]APIKey, error) { +// GetMachineKeys list machine API keys. +func (c *Client) GetMachineKeys(ctx context.Context, userID string) ([]APIKey, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/machine", userID), nil) if err != nil { return nil, err diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5322135001b1a..44fdc43397ada 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1,6 +1,6 @@ // Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT. -// From codersdk/api_key.go +// From codersdk/apikey.go export interface APIKey { readonly id: string readonly user_id: string @@ -685,7 +685,7 @@ export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" // From codersdk/provisionerdaemons.go export type LogSource = "provisioner" | "provisioner_daemon" -// From codersdk/api_key.go +// From codersdk/apikey.go export type LoginType = "github" | "machine" | "oidc" | "password" // From codersdk/parameters.go From f2c5e13e4f3392591ce4a68cfda2e670398b4ab6 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 16:49:39 +0000 Subject: [PATCH 09/17] lint --- cli/keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/keys.go b/cli/keys.go index 974623c6a1370..aa3ec056bf61b 100644 --- a/cli/keys.go +++ b/cli/keys.go @@ -64,7 +64,7 @@ func createKey() *cobra.Command { cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key))) cmd.Println() cmd.Println(cliui.Styles.Wrap.Render( - fmt.Sprintf("You can use this API key by setting --%s CLI flag, the %s environment variable, or the \"%s\" HTTP header.", varToken, envSessionToken, codersdk.SessionTokenKey), + fmt.Sprintf("You can use this API key by setting --%s CLI flag, the %s environment variable, or the %q HTTP header.", varToken, envSessionToken, codersdk.SessionTokenKey), )) return nil From 8d99b7633c657e597038e978a2fd733a2cf240ff Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 16:57:37 +0000 Subject: [PATCH 10/17] fix cliui test --- cli/cliui/table_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/cliui/table_test.go b/cli/cliui/table_test.go index 54fc307b73bd2..b369ed1a996a2 100644 --- a/cli/cliui/table_test.go +++ b/cli/cliui/table_test.go @@ -132,9 +132,9 @@ func Test_DisplayTable(t *testing.T) { expected := ` NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR -foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10 -bar 20 [a] bar1 21 bar3 23 {bar4 24 } Aug 2 15:49:10 -baz 30 [] baz1 31 baz3 33 {baz4 34 } Aug 2 15:49:10 +foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z +bar 20 [a] bar1 21 bar3 23 {bar4 24 } 2022-08-02T15:49:10Z +baz 30 [] baz1 31 baz3 33 {baz4 34 } 2022-08-02T15:49:10Z ` // Test with non-pointer values. @@ -159,9 +159,9 @@ baz 30 [] baz1 31 baz3 expected := ` NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR -bar 20 [a] bar1 21 bar3 23 {bar4 24 } Aug 2 15:49:10 -baz 30 [] baz1 31 baz3 33 {baz4 34 } Aug 2 15:49:10 -foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10 +bar 20 [a] bar1 21 bar3 23 {bar4 24 } 2022-08-02T15:49:10Z +baz 30 [] baz1 31 baz3 33 {baz4 34 } 2022-08-02T15:49:10Z +foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z ` out, err := cliui.DisplayTable(in, "name", nil) @@ -175,9 +175,9 @@ foo 10 [a b c] foo1 11 foo2 12 foo3 expected := ` NAME SUB 1 NAME SUB 3 INNER NAME TIME -foo foo1 foo3 Aug 2 15:49:10 -bar bar1 bar3 Aug 2 15:49:10 -baz baz1 baz3 Aug 2 15:49:10 +foo foo1 foo3 2022-08-02T15:49:10Z +bar bar1 bar3 2022-08-02T15:49:10Z +baz baz1 baz3 2022-08-02T15:49:10Z ` out, err := cliui.DisplayTable(in, "", []string{"name", "sub_1_name", "sub_3 inner name", "time"}) From 07d1d7969684e2b5253a15fb60d4396b75d6ac27 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 16:58:30 +0000 Subject: [PATCH 11/17] fix alignment --- cli/cliui/table_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/cliui/table_test.go b/cli/cliui/table_test.go index b369ed1a996a2..7e28eb28a336e 100644 --- a/cli/cliui/table_test.go +++ b/cli/cliui/table_test.go @@ -131,7 +131,7 @@ func Test_DisplayTable(t *testing.T) { t.Parallel() expected := ` -NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR +NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z bar 20 [a] bar1 21 bar3 23 {bar4 24 } 2022-08-02T15:49:10Z baz 30 [] baz1 31 baz3 33 {baz4 34 } 2022-08-02T15:49:10Z @@ -158,7 +158,7 @@ baz 30 [] baz1 31 baz3 t.Parallel() expected := ` -NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR +NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR bar 20 [a] bar1 21 bar3 23 {bar4 24 } 2022-08-02T15:49:10Z baz 30 [] baz1 31 baz3 33 {baz4 34 } 2022-08-02T15:49:10Z foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z From 5a94dc8cb25a542a042d832b9b2b3d383bc27d76 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 17:03:27 +0000 Subject: [PATCH 12/17] migration conflict --- coderd/database/migrations/000056_api_key_application.down.sql | 2 -- coderd/database/migrations/000056_api_key_application.up.sql | 1 - 2 files changed, 3 deletions(-) delete mode 100644 coderd/database/migrations/000056_api_key_application.down.sql delete mode 100644 coderd/database/migrations/000056_api_key_application.up.sql diff --git a/coderd/database/migrations/000056_api_key_application.down.sql b/coderd/database/migrations/000056_api_key_application.down.sql deleted file mode 100644 index 0d2e97aef130e..0000000000000 --- a/coderd/database/migrations/000056_api_key_application.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- You cannot safely remove values from enums https://www.postgresql.org/docs/current/datatype-enum.html --- You cannot create a new type and do a rename because objects depend on this type now. diff --git a/coderd/database/migrations/000056_api_key_application.up.sql b/coderd/database/migrations/000056_api_key_application.up.sql deleted file mode 100644 index 3580bdc1b809f..0000000000000 --- a/coderd/database/migrations/000056_api_key_application.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TYPE login_type ADD VALUE IF NOT EXISTS 'machine'; From 8b10c269e06b494e4bdceec2770e58a379836bf2 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 18:00:22 +0000 Subject: [PATCH 13/17] new migration --- coderd/database/migrations/000057_api_key_application.down.sql | 2 ++ coderd/database/migrations/000057_api_key_application.up.sql | 1 + 2 files changed, 3 insertions(+) create mode 100644 coderd/database/migrations/000057_api_key_application.down.sql create mode 100644 coderd/database/migrations/000057_api_key_application.up.sql diff --git a/coderd/database/migrations/000057_api_key_application.down.sql b/coderd/database/migrations/000057_api_key_application.down.sql new file mode 100644 index 0000000000000..0d2e97aef130e --- /dev/null +++ b/coderd/database/migrations/000057_api_key_application.down.sql @@ -0,0 +1,2 @@ +-- You cannot safely remove values from enums https://www.postgresql.org/docs/current/datatype-enum.html +-- You cannot create a new type and do a rename because objects depend on this type now. diff --git a/coderd/database/migrations/000057_api_key_application.up.sql b/coderd/database/migrations/000057_api_key_application.up.sql new file mode 100644 index 0000000000000..3580bdc1b809f --- /dev/null +++ b/coderd/database/migrations/000057_api_key_application.up.sql @@ -0,0 +1 @@ +ALTER TYPE login_type ADD VALUE IF NOT EXISTS 'machine'; From 0db2bf6d93f52030d275c5570059e47cb44fb53b Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 18:28:25 +0000 Subject: [PATCH 14/17] rename to tokens --- cli/keys.go | 62 +++++++++---------- cli/keys_test.go | 10 +-- cli/root.go | 2 +- coderd/apikey.go | 14 ++--- coderd/apikey_test.go | 8 +-- coderd/coderd.go | 6 +- coderd/database/dump.sql | 2 +- .../000057_api_key_application.down.sql | 2 - .../000057_api_key_application.up.sql | 1 - coderd/database/models.go | 2 +- coderd/users_test.go | 4 +- codersdk/apikey.go | 14 ++--- site/src/api/typesGenerated.ts | 2 +- 13 files changed, 61 insertions(+), 68 deletions(-) delete mode 100644 coderd/database/migrations/000057_api_key_application.down.sql delete mode 100644 coderd/database/migrations/000057_api_key_application.up.sql diff --git a/cli/keys.go b/cli/keys.go index aa3ec056bf61b..3ad22f14f2ca9 100644 --- a/cli/keys.go +++ b/cli/keys.go @@ -12,59 +12,59 @@ import ( "github.com/coder/coder/codersdk" ) -func keys() *cobra.Command { +func tokens() *cobra.Command { cmd := &cobra.Command{ - Use: "keys", - Short: "Manage machine keys", - Long: "Machine keys are used to authenticate automated clients to Coder.", - Aliases: []string{"key"}, + Use: "tokens", + Short: "Manage personal access tokens", + Long: "Tokens are used to authenticate automated clients to Coder.", + Aliases: []string{"token"}, Example: formatExamples( example{ - Description: "Create a machine key for CI/CD scripts", - Command: "coder keys create", + Description: "Create a token for automation", + Command: "coder tokens create", }, example{ - Description: "List your machine keys", - Command: "coder keys ls", + Description: "List your tokens", + Command: "coder tokens ls", }, example{ - Description: "Remove a key by ID", - Command: "coder keys rm WuoWs4ZsMX", + Description: "Remove a token by ID", + Command: "coder tokens rm WuoWs4ZsMX", }, ), } cmd.AddCommand( - createKey(), - listKeys(), - removeKey(), + createToken(), + listTokens(), + removeToken(), ) return cmd } -func createKey() *cobra.Command { +func createToken() *cobra.Command { cmd := &cobra.Command{ Use: "create", - Short: "Create a machine key", + Short: "Create a tokens", RunE: func(cmd *cobra.Command, args []string) error { client, err := CreateClient(cmd) if err != nil { return xerrors.Errorf("create codersdk client: %w", err) } - res, err := client.CreateMachineKey(cmd.Context(), codersdk.Me) + res, err := client.CreateToken(cmd.Context(), codersdk.Me) if err != nil { - return xerrors.Errorf("create machine key: %w", err) + return xerrors.Errorf("create tokens: %w", err) } cmd.Println(cliui.Styles.Wrap.Render( - "Here is your API key. 🪄", + "Here is your token. 🪄", )) cmd.Println() cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key))) cmd.Println() cmd.Println(cliui.Styles.Wrap.Render( - fmt.Sprintf("You can use this API key by setting --%s CLI flag, the %s environment variable, or the %q HTTP header.", varToken, envSessionToken, codersdk.SessionTokenKey), + fmt.Sprintf("You can use this token by setting the --%s CLI flag, the %s environment variable, or the %q HTTP header.", varToken, envSessionToken, codersdk.SessionTokenKey), )) return nil @@ -74,38 +74,38 @@ func createKey() *cobra.Command { return cmd } -type keyRow struct { +type tokenRow struct { ID string `table:"ID"` LastUsed time.Time `table:"Last Used"` ExpiresAt time.Time `table:"Expires At"` CreatedAt time.Time `table:"Created At"` } -func listKeys() *cobra.Command { +func listTokens() *cobra.Command { cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, - Short: "List machine keys", + Short: "List tokens", RunE: func(cmd *cobra.Command, args []string) error { client, err := CreateClient(cmd) if err != nil { return xerrors.Errorf("create codersdk client: %w", err) } - keys, err := client.GetMachineKeys(cmd.Context(), codersdk.Me) + keys, err := client.GetTokens(cmd.Context(), codersdk.Me) if err != nil { - return xerrors.Errorf("create machine key: %w", err) + return xerrors.Errorf("create tokens: %w", err) } if len(keys) == 0 { cmd.Println(cliui.Styles.Wrap.Render( - "No machine keys found.", + "No tokens found.", )) } - var rows []keyRow + var rows []tokenRow for _, key := range keys { - rows = append(rows, keyRow{ + rows = append(rows, tokenRow{ ID: key.ID, LastUsed: key.LastUsed, ExpiresAt: key.ExpiresAt, @@ -126,11 +126,11 @@ func listKeys() *cobra.Command { return cmd } -func removeKey() *cobra.Command { +func removeToken() *cobra.Command { cmd := &cobra.Command{ Use: "remove [id]", Aliases: []string{"rm"}, - Short: "Delete a machine key", + Short: "Delete a token", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := CreateClient(cmd) @@ -144,7 +144,7 @@ func removeKey() *cobra.Command { } cmd.Println(cliui.Styles.Wrap.Render( - "API key has been deleted.", + "Token has been deleted.", )) return nil diff --git a/cli/keys_test.go b/cli/keys_test.go index 8b554f5473ffd..c4e5e838250f2 100644 --- a/cli/keys_test.go +++ b/cli/keys_test.go @@ -17,16 +17,16 @@ func TestMachineKeys(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) // helpful empty response - cmd, root := clitest.New(t, "keys", "ls") + cmd, root := clitest.New(t, "tokens", "ls") clitest.SetupConfig(t, client, root) buf := new(bytes.Buffer) cmd.SetOut(buf) err := cmd.Execute() require.NoError(t, err) res := buf.String() - require.Contains(t, res, "keys found") + require.Contains(t, res, "tokens found") - cmd, root = clitest.New(t, "keys", "create") + cmd, root = clitest.New(t, "tokens", "create") clitest.SetupConfig(t, client, root) buf = new(bytes.Buffer) cmd.SetOut(buf) @@ -40,7 +40,7 @@ func TestMachineKeys(t *testing.T) { key := r.FindString(res) id := key[:10] - cmd, root = clitest.New(t, "keys", "ls") + cmd, root = clitest.New(t, "tokens", "ls") clitest.SetupConfig(t, client, root) buf = new(bytes.Buffer) cmd.SetOut(buf) @@ -54,7 +54,7 @@ func TestMachineKeys(t *testing.T) { require.Contains(t, res, "LAST USED") require.Contains(t, res, id) - cmd, root = clitest.New(t, "keys", "rm", id) + cmd, root = clitest.New(t, "tokens", "rm", id) clitest.SetupConfig(t, client, root) buf = new(bytes.Buffer) cmd.SetOut(buf) diff --git a/cli/root.go b/cli/root.go index e90c1e174522e..0f533a5b1c9ff 100644 --- a/cli/root.go +++ b/cli/root.go @@ -93,7 +93,7 @@ func Core() []*cobra.Command { users(), versionCmd(), workspaceAgent(), - keys(), + tokens(), } } diff --git a/coderd/apikey.go b/coderd/apikey.go index 78833e5a248c5..4fb322b763d56 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -24,8 +24,8 @@ import ( "github.com/coder/coder/cryptorand" ) -// Creates a new machine session key that effectively doesn't expire. -func (api *API) postMachineAPIKey(rw http.ResponseWriter, r *http.Request) { +// Creates a new token API key that effectively doesn't expire. +func (api *API) postToken(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() user := httpmw.UserParam(r) @@ -38,7 +38,7 @@ func (api *API) postMachineAPIKey(rw http.ResponseWriter, r *http.Request) { lifeTime := time.Hour * 876000 cookie, err := api.createAPIKey(ctx, createAPIKeyParams{ UserID: user.ID, - LoginType: database.LoginTypeMachine, + LoginType: database.LoginTypeToken, ExpiresAt: database.Now().Add(lifeTime), LifetimeSeconds: int64(lifeTime.Seconds()), }) @@ -50,10 +50,6 @@ func (api *API) postMachineAPIKey(rw http.ResponseWriter, r *http.Request) { return } - // We intentionally do not set the cookie on the response here. - // Setting the cookie will couple the browser sesion to the API - // key we return here, meaning logging out of the website would - // invalid your CLI key. httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value}) } @@ -85,7 +81,7 @@ func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key)) } -func (api *API) machineKeys(rw http.ResponseWriter, r *http.Request) { +func (api *API) tokens(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() user = httpmw.UserParam(r) @@ -96,7 +92,7 @@ func (api *API) machineKeys(rw http.ResponseWriter, r *http.Request) { return } - keys, err := api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeMachine) + keys, err := api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeToken) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusOK, []codersdk.APIKey{}) return diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go index 181a5fef665a8..f7b2dc40488e3 100644 --- a/coderd/apikey_test.go +++ b/coderd/apikey_test.go @@ -18,15 +18,15 @@ func TestMachineKeys(t *testing.T) { defer cancel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) _ = coderdtest.CreateFirstUser(t, client) - keys, err := client.GetMachineKeys(ctx, codersdk.Me) + keys, err := client.GetTokens(ctx, codersdk.Me) require.NoError(t, err) require.Empty(t, keys) - res, err := client.CreateMachineKey(ctx, codersdk.Me) + res, err := client.CreateToken(ctx, codersdk.Me) require.NoError(t, err) require.Greater(t, len(res.Key), 2) - keys, err = client.GetMachineKeys(ctx, codersdk.Me) + keys, err = client.GetTokens(ctx, codersdk.Me) require.NoError(t, err) require.EqualValues(t, len(keys), 1) require.Contains(t, res.Key, keys[0].ID) @@ -35,7 +35,7 @@ func TestMachineKeys(t *testing.T) { err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID) require.NoError(t, err) - keys, err = client.GetMachineKeys(ctx, codersdk.Me) + keys, err = client.GetTokens(ctx, codersdk.Me) require.NoError(t, err) require.Empty(t, keys) } diff --git a/coderd/coderd.go b/coderd/coderd.go index 786d4737be2c7..5a1719cd3790c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -399,9 +399,9 @@ func New(options *Options) *API { r.Get("/roles", api.userRoles) r.Route("/keys", func(r chi.Router) { - r.Route("/machine", func(r chi.Router) { - r.Post("/", api.postMachineAPIKey) - r.Get("/", api.machineKeys) + r.Route("/tokens", func(r chi.Router) { + r.Post("/", api.postToken) + r.Get("/", api.tokens) }) r.Route("/{keyid}", func(r chi.Router) { r.Get("/", api.apiKey) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 78f55cbc051db..1c7c5b5f19477 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -34,7 +34,7 @@ CREATE TYPE login_type AS ENUM ( 'password', 'github', 'oidc', - 'machine' + 'token' ); CREATE TYPE parameter_destination_scheme AS ENUM ( diff --git a/coderd/database/migrations/000057_api_key_application.down.sql b/coderd/database/migrations/000057_api_key_application.down.sql deleted file mode 100644 index 0d2e97aef130e..0000000000000 --- a/coderd/database/migrations/000057_api_key_application.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- You cannot safely remove values from enums https://www.postgresql.org/docs/current/datatype-enum.html --- You cannot create a new type and do a rename because objects depend on this type now. diff --git a/coderd/database/migrations/000057_api_key_application.up.sql b/coderd/database/migrations/000057_api_key_application.up.sql deleted file mode 100644 index 3580bdc1b809f..0000000000000 --- a/coderd/database/migrations/000057_api_key_application.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TYPE login_type ADD VALUE IF NOT EXISTS 'machine'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 8e7fc54ff8e9b..9cfade4341c93 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -120,7 +120,7 @@ const ( LoginTypePassword LoginType = "password" LoginTypeGithub LoginType = "github" LoginTypeOIDC LoginType = "oidc" - LoginTypeMachine LoginType = "machine" + LoginTypeToken LoginType = "token" ) func (e *LoginType) Scan(src interface{}) error { diff --git a/coderd/users_test.go b/coderd/users_test.go index 42f95509b01a8..129d09504ae38 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -286,7 +286,7 @@ func TestPostLogin(t *testing.T) { require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400") // Machine tokens have a longer life - token, err := client.CreateMachineKey(ctx, codersdk.Me) + token, err := client.CreateToken(ctx, codersdk.Me) require.NoError(t, err, "make new machine api key") split = strings.Split(token.Key, "-") apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0]) @@ -1202,7 +1202,7 @@ func TestPostMachineKey(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - apiKey, err := client.CreateMachineKey(ctx, codersdk.Me) + apiKey, err := client.CreateToken(ctx, codersdk.Me) require.NotNil(t, apiKey) require.GreaterOrEqual(t, len(apiKey.Key), 2) require.NoError(t, err) diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 8eee3ca223dcf..3be9a53b5d2df 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -28,12 +28,12 @@ const ( LoginTypePassword LoginType = "password" LoginTypeGithub LoginType = "github" LoginTypeOIDC LoginType = "oidc" - LoginTypeMachine LoginType = "machine" + LoginTypeToken LoginType = "token" ) -// CreateMachineKey generates an API key that doesn't expire. -func (c *Client) CreateMachineKey(ctx context.Context, userID string) (*GenerateAPIKeyResponse, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/machine", userID), nil) +// CreateToken generates an API key that doesn't expire. +func (c *Client) CreateToken(ctx context.Context, userID string) (*GenerateAPIKeyResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil) if err != nil { return nil, err } @@ -45,9 +45,9 @@ func (c *Client) CreateMachineKey(ctx context.Context, userID string) (*Generate return apiKey, json.NewDecoder(res.Body).Decode(apiKey) } -// GetMachineKeys list machine API keys. -func (c *Client) GetMachineKeys(ctx context.Context, userID string) ([]APIKey, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/machine", userID), nil) +// GetTokens list machine API keys. +func (c *Client) GetTokens(ctx context.Context, userID string) ([]APIKey, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil) if err != nil { return nil, err } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 44fdc43397ada..ce6fca90a0e3e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -686,7 +686,7 @@ export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" export type LogSource = "provisioner" | "provisioner_daemon" // From codersdk/apikey.go -export type LoginType = "github" | "machine" | "oidc" | "password" +export type LoginType = "github" | "oidc" | "password" | "token" // From codersdk/parameters.go export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable" From f4dbe39e70be0cd07c14bb77dcada39c06c88056 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 18:28:33 +0000 Subject: [PATCH 15/17] rename to tokens --- coderd/database/migrations/000057_api_key_token.down.sql | 2 ++ coderd/database/migrations/000057_api_key_token.up.sql | 1 + 2 files changed, 3 insertions(+) create mode 100644 coderd/database/migrations/000057_api_key_token.down.sql create mode 100644 coderd/database/migrations/000057_api_key_token.up.sql diff --git a/coderd/database/migrations/000057_api_key_token.down.sql b/coderd/database/migrations/000057_api_key_token.down.sql new file mode 100644 index 0000000000000..0d2e97aef130e --- /dev/null +++ b/coderd/database/migrations/000057_api_key_token.down.sql @@ -0,0 +1,2 @@ +-- You cannot safely remove values from enums https://www.postgresql.org/docs/current/datatype-enum.html +-- You cannot create a new type and do a rename because objects depend on this type now. diff --git a/coderd/database/migrations/000057_api_key_token.up.sql b/coderd/database/migrations/000057_api_key_token.up.sql new file mode 100644 index 0000000000000..bb03e2f70950b --- /dev/null +++ b/coderd/database/migrations/000057_api_key_token.up.sql @@ -0,0 +1 @@ +ALTER TYPE login_type ADD VALUE IF NOT EXISTS 'token'; From 0537ad2a4693a5d34a19904f7a999e34f84a65b2 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 18:47:25 +0000 Subject: [PATCH 16/17] more renames --- cli/{keys.go => tokens.go} | 0 cli/{keys_test.go => tokens_test.go} | 2 +- coderd/apikey_test.go | 2 +- coderd/users_test.go | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename cli/{keys.go => tokens.go} (100%) rename cli/{keys_test.go => tokens_test.go} (97%) diff --git a/cli/keys.go b/cli/tokens.go similarity index 100% rename from cli/keys.go rename to cli/tokens.go diff --git a/cli/keys_test.go b/cli/tokens_test.go similarity index 97% rename from cli/keys_test.go rename to cli/tokens_test.go index c4e5e838250f2..de591c7701c40 100644 --- a/cli/keys_test.go +++ b/cli/tokens_test.go @@ -11,7 +11,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" ) -func TestMachineKeys(t *testing.T) { +func TestTokens(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go index f7b2dc40488e3..8fbccd783faf3 100644 --- a/coderd/apikey_test.go +++ b/coderd/apikey_test.go @@ -12,7 +12,7 @@ import ( "github.com/coder/coder/testutil" ) -func TestMachineKeys(t *testing.T) { +func TestTokens(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() diff --git a/coderd/users_test.go b/coderd/users_test.go index 129d09504ae38..3a802c6c168b1 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1194,7 +1194,7 @@ func TestGetUsers(t *testing.T) { }) } -func TestPostMachineKey(t *testing.T) { +func TestPostTokens(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) From 822646462a08da917a0303d187d5bd5e00851d40 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Oct 2022 18:53:15 +0000 Subject: [PATCH 17/17] more renames --- coderd/apikey.go | 2 +- coderd/users_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/apikey.go b/coderd/apikey.go index 4fb322b763d56..ba5c7c11aa8d3 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -34,7 +34,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) { return } - // machine keys last 100 years + // tokens last 100 years lifeTime := time.Hour * 876000 cookie, err := api.createAPIKey(ctx, createAPIKeyParams{ UserID: user.ID, diff --git a/coderd/users_test.go b/coderd/users_test.go index 3a802c6c168b1..3a7a11b67074d 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -285,15 +285,15 @@ func TestPostLogin(t *testing.T) { require.NoError(t, err, "fetch login key") require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400") - // Machine tokens have a longer life + // tokens have a longer life token, err := client.CreateToken(ctx, codersdk.Me) - require.NoError(t, err, "make new machine api key") + require.NoError(t, err, "make new token api key") split = strings.Split(token.Key, "-") apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0]) require.NoError(t, err, "fetch api key") - require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*438300)), "api key lasts more than 50 years") - require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "api key should have longer lifetime") + require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*438300)), "tokens lasts more than 50 years") + require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime") }) }