From b100d9cd7322f4e215ab18734b131d45783cdf5c Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Wed, 24 Feb 2021 02:39:21 +0000 Subject: [PATCH 1/4] feat: enable password changes in coder-sdk * Allow modification of passwords and the temporary_password flag for users managed by the built-in authentication provider. * Split unit tests into separate files corresponding to the implementations. --- coder-sdk/activity_test.go | 50 ++++++++++++++++++ coder-sdk/client_test.go | 82 ----------------------------- coder-sdk/users.go | 23 ++++++++- coder-sdk/users_test.go | 102 +++++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 83 deletions(-) create mode 100644 coder-sdk/activity_test.go create mode 100644 coder-sdk/users_test.go diff --git a/coder-sdk/activity_test.go b/coder-sdk/activity_test.go new file mode 100644 index 00000000..de325992 --- /dev/null +++ b/coder-sdk/activity_test.go @@ -0,0 +1,50 @@ +package coder_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "cdr.dev/coder-cli/coder-sdk" + "github.com/stretchr/testify/require" +) + +func TestPushActivity(t *testing.T) { + t.Parallel() + + const source = "test" + const envID = "602d377a-e6b8d763cae7561885c5f1b2" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method, "PushActivity is a POST") + require.Equal(t, "/api/private/metrics/usage/push", r.URL.Path) + + expected := map[string]interface{}{ + "source": source, + "environment_id": envID, + } + var request map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&request) + require.NoError(t, err, "error decoding JSON") + require.EqualValues(t, expected, request, "unexpected request data") + + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(func() { + server.Close() + }) + + u, err := url.Parse(server.URL) + require.NoError(t, err, "failed to parse test server URL") + + client, err := coder.NewClient(coder.ClientOptions{ + BaseURL: u, + Token: "SwdcSoq5Jc-0C1r8wfwm7h6h9i0RDk7JT", + }) + require.NoError(t, err, "failed to create coder.Client") + + err = client.PushActivity(context.Background(), source, envID) + require.NoError(t, err) +} diff --git a/coder-sdk/client_test.go b/coder-sdk/client_test.go index a32716b1..a1a4ecfa 100644 --- a/coder-sdk/client_test.go +++ b/coder-sdk/client_test.go @@ -14,88 +14,6 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) -func TestPushActivity(t *testing.T) { - t.Parallel() - - const source = "test" - const envID = "602d377a-e6b8d763cae7561885c5f1b2" - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method, "PushActivity is a POST") - require.Equal(t, "/api/private/metrics/usage/push", r.URL.Path) - - expected := map[string]interface{}{ - "source": source, - "environment_id": envID, - } - var request map[string]interface{} - err := json.NewDecoder(r.Body).Decode(&request) - require.NoError(t, err, "error decoding JSON") - require.EqualValues(t, expected, request, "unexpected request data") - - w.WriteHeader(http.StatusOK) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - require.NoError(t, err, "failed to parse test server URL") - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "SwdcSoq5Jc-0C1r8wfwm7h6h9i0RDk7JT", - }) - require.NoError(t, err, "failed to create coder.Client") - - err = client.PushActivity(context.Background(), source, envID) - require.NoError(t, err) -} - -func TestUsers(t *testing.T) { - t.Parallel() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method, "Users is a GET") - require.Equal(t, "/api/v0/users", r.URL.Path) - - users := []map[string]interface{}{ - { - "id": "default", - "email": "root@user.com", - "username": "root", - "name": "Charlie Root", - "roles": []coder.Role{coder.SiteAdmin}, - "temporary_password": false, - "login_type": coder.LoginTypeBuiltIn, - "key_regenerated_at": time.Now(), - "created_at": time.Now(), - "updated_at": time.Now(), - }, - } - - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(users) - require.NoError(t, err, "error encoding JSON") - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - require.NoError(t, err, "failed to parse test server URL") - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", - }) - require.NoError(t, err, "failed to create coder.Client") - - users, err := client.Users(context.Background()) - require.NoError(t, err, "error getting Users") - require.Len(t, users, 1, "users should return a single user") - require.Equal(t, "Charlie Root", users[0].Name) - require.Equal(t, "root", users[0].Username) -} func TestAuthentication(t *testing.T) { t.Parallel() diff --git a/coder-sdk/users.go b/coder-sdk/users.go index d53c6fab..64b1fe51 100644 --- a/coder-sdk/users.go +++ b/coder-sdk/users.go @@ -99,7 +99,7 @@ func (c *DefaultClient) UserByEmail(ctx context.Context, email string) (*User, e // UpdateUserReq defines a modification to the user, updating the // value of all non-nil values. type UpdateUserReq struct { - // TODO(@cmoog) add update password option + *UserPasswordSettings Revoked *bool `json:"revoked,omitempty"` Roles *[]Role `json:"roles,omitempty"` LoginType *LoginType `json:"login_type,omitempty"` @@ -109,6 +109,27 @@ type UpdateUserReq struct { DotfilesGitURL *string `json:"dotfiles_git_uri,omitempty"` } +// UserPasswordSettings allows modification of the user's password +// settings. +// +// These settings are only applicable to users managed using the +// built-in authentication provider; users authenticating using +// OAuth must change their password through the identity provider +// instead. +type UserPasswordSettings struct { + // OldPassword is the account's current password. + OldPassword string `json:"old_password,omitempty"` + + // Password is the new password, which may be a temporary password. + Password string `json:"password,omitempty"` + + // Temporary indicates that API access should be restricted to the + // password change API and a few other APIs. If set to true, Coder + // will prompt the user to change their password upon their next + // login through the web interface. + Temporary bool `json:"temporary_password,omitempty"` +} + // UpdateUser applyes the partial update to the given user. func (c *DefaultClient) UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/v0/users/"+userID, req, nil) diff --git a/coder-sdk/users_test.go b/coder-sdk/users_test.go new file mode 100644 index 00000000..ce13c031 --- /dev/null +++ b/coder-sdk/users_test.go @@ -0,0 +1,102 @@ +package coder_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "cdr.dev/coder-cli/coder-sdk" + "github.com/stretchr/testify/require" +) + +func TestUsers(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method, "Users is a GET") + require.Equal(t, "/api/v0/users", r.URL.Path) + + users := []map[string]interface{}{ + { + "id": "default", + "email": "root@user.com", + "username": "root", + "name": "Charlie Root", + "roles": []coder.Role{coder.SiteAdmin}, + "temporary_password": false, + "login_type": coder.LoginTypeBuiltIn, + "key_regenerated_at": time.Now(), + "created_at": time.Now(), + "updated_at": time.Now(), + }, + } + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(users) + require.NoError(t, err, "error encoding JSON") + })) + t.Cleanup(func() { + server.Close() + }) + + u, err := url.Parse(server.URL) + require.NoError(t, err, "failed to parse test server URL") + + client, err := coder.NewClient(coder.ClientOptions{ + BaseURL: u, + Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", + }) + require.NoError(t, err, "failed to create coder.Client") + + users, err := client.Users(context.Background()) + require.NoError(t, err, "error getting Users") + require.Len(t, users, 1, "users should return a single user") + require.Equal(t, "Charlie Root", users[0].Name) + require.Equal(t, "root", users[0].Username) +} + +func TestUserUpdatePassword(t *testing.T) { + t.Parallel() + + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPatch, r.Method, "Users is a PATCH") + require.Equal(t, "/api/v0/users/me", r.URL.Path) + + expected := map[string]interface{}{ + "old_password": "vt9g9rxsptrq", + "password": "wmf39jw2f7pk", + "temporary_password": true, + } + var request map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&request) + require.NoError(t, err, "error decoding JSON") + require.EqualValues(t, expected, request, "unexpected request data") + + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(func() { + server.Close() + }) + + u, err := url.Parse(server.URL) + require.NoError(t, err, "failed to parse test server URL") + + client, err := coder.NewClient(coder.ClientOptions{ + BaseURL: u, + HTTPClient: server.Client(), + Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", + }) + require.NoError(t, err, "failed to create coder.Client") + + client.UpdateUser(context.Background(), "me", coder.UpdateUserReq{ + UserPasswordSettings: &coder.UserPasswordSettings{ + OldPassword: "vt9g9rxsptrq", + Password: "wmf39jw2f7pk", + Temporary: true, + }, + }) +} From a335df95f1d480df55227f89b628765f9c71eccc Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Wed, 24 Feb 2021 19:02:16 +0000 Subject: [PATCH 2/4] lint and format fixes --- coder-sdk/activity_test.go | 3 ++- coder-sdk/client_test.go | 1 - coder-sdk/test/login_cmd.go | 51 +++++++++++++++++++++++++++++++++++++ coder-sdk/users_test.go | 6 +++-- 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 coder-sdk/test/login_cmd.go diff --git a/coder-sdk/activity_test.go b/coder-sdk/activity_test.go index de325992..778ba48a 100644 --- a/coder-sdk/activity_test.go +++ b/coder-sdk/activity_test.go @@ -8,8 +8,9 @@ import ( "net/url" "testing" - "cdr.dev/coder-cli/coder-sdk" "github.com/stretchr/testify/require" + + "cdr.dev/coder-cli/coder-sdk" ) func TestPushActivity(t *testing.T) { diff --git a/coder-sdk/client_test.go b/coder-sdk/client_test.go index a1a4ecfa..a538626d 100644 --- a/coder-sdk/client_test.go +++ b/coder-sdk/client_test.go @@ -14,7 +14,6 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) - func TestAuthentication(t *testing.T) { t.Parallel() diff --git a/coder-sdk/test/login_cmd.go b/coder-sdk/test/login_cmd.go new file mode 100644 index 00000000..2d0a7ab6 --- /dev/null +++ b/coder-sdk/test/login_cmd.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "fmt" + "net/url" + + "cdr.dev/coder-cli/coder-sdk" +) + +func main() { + client, err := coder.NewClient(coder.ClientOptions{ + BaseURL: &url.URL{ + Scheme: "http", + Host: "localhost:8080", + }, + Email: "admin", + Password: "vt9g9rxsptrq", + }) + if err != nil { + fmt.Printf("login error: %v\n", err) + return + } + + user, err := client.Me(context.Background()) + if err != nil { + fmt.Printf("Me error: %v\n", err) + return + } + + fmt.Printf("user info: %#v\n", user) + + fmt.Println("changing password") + err = client.UpdateUser(context.Background(), "me", coder.UpdateUserReq{ + UserPasswordSettings: &coder.UserPasswordSettings{ + Password: "szbp4q3bcrhc", + }, + }) + if err != nil { + fmt.Printf("password update error: %v\n", err) + return + } + + orgs, err := client.Organizations(context.Background()) + if err != nil { + fmt.Printf("org list error: %v\n", err) + return + } + + fmt.Printf("orgs info: %#v\n", orgs) +} diff --git a/coder-sdk/users_test.go b/coder-sdk/users_test.go index ce13c031..9ab27fee 100644 --- a/coder-sdk/users_test.go +++ b/coder-sdk/users_test.go @@ -9,8 +9,9 @@ import ( "testing" "time" - "cdr.dev/coder-cli/coder-sdk" "github.com/stretchr/testify/require" + + "cdr.dev/coder-cli/coder-sdk" ) func TestUsers(t *testing.T) { @@ -92,11 +93,12 @@ func TestUserUpdatePassword(t *testing.T) { }) require.NoError(t, err, "failed to create coder.Client") - client.UpdateUser(context.Background(), "me", coder.UpdateUserReq{ + err = client.UpdateUser(context.Background(), "me", coder.UpdateUserReq{ UserPasswordSettings: &coder.UserPasswordSettings{ OldPassword: "vt9g9rxsptrq", Password: "wmf39jw2f7pk", Temporary: true, }, }) + require.NoError(t, err, "error when updating password") } From 1350d793bbf903b29348afb87091b3e98d8401a1 Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Thu, 25 Feb 2021 00:04:56 +0000 Subject: [PATCH 3/4] code review --- coder-sdk/test/login_cmd.go | 51 ------------------------------------- coder-sdk/users_test.go | 4 +-- 2 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 coder-sdk/test/login_cmd.go diff --git a/coder-sdk/test/login_cmd.go b/coder-sdk/test/login_cmd.go deleted file mode 100644 index 2d0a7ab6..00000000 --- a/coder-sdk/test/login_cmd.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "context" - "fmt" - "net/url" - - "cdr.dev/coder-cli/coder-sdk" -) - -func main() { - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: &url.URL{ - Scheme: "http", - Host: "localhost:8080", - }, - Email: "admin", - Password: "vt9g9rxsptrq", - }) - if err != nil { - fmt.Printf("login error: %v\n", err) - return - } - - user, err := client.Me(context.Background()) - if err != nil { - fmt.Printf("Me error: %v\n", err) - return - } - - fmt.Printf("user info: %#v\n", user) - - fmt.Println("changing password") - err = client.UpdateUser(context.Background(), "me", coder.UpdateUserReq{ - UserPasswordSettings: &coder.UserPasswordSettings{ - Password: "szbp4q3bcrhc", - }, - }) - if err != nil { - fmt.Printf("password update error: %v\n", err) - return - } - - orgs, err := client.Organizations(context.Background()) - if err != nil { - fmt.Printf("org list error: %v\n", err) - return - } - - fmt.Printf("orgs info: %#v\n", orgs) -} diff --git a/coder-sdk/users_test.go b/coder-sdk/users_test.go index 9ab27fee..d84dc5c6 100644 --- a/coder-sdk/users_test.go +++ b/coder-sdk/users_test.go @@ -70,7 +70,7 @@ func TestUserUpdatePassword(t *testing.T) { expected := map[string]interface{}{ "old_password": "vt9g9rxsptrq", "password": "wmf39jw2f7pk", - "temporary_password": true, + "temporary_password": false, } var request map[string]interface{} err := json.NewDecoder(r.Body).Decode(&request) @@ -97,7 +97,7 @@ func TestUserUpdatePassword(t *testing.T) { UserPasswordSettings: &coder.UserPasswordSettings{ OldPassword: "vt9g9rxsptrq", Password: "wmf39jw2f7pk", - Temporary: true, + Temporary: false, }, }) require.NoError(t, err, "error when updating password") From 96bdd845c935acf7511cafaca4ec9d736cd1c8ff Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Thu, 25 Feb 2021 00:09:10 +0000 Subject: [PATCH 4/4] fix test --- coder-sdk/users_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/coder-sdk/users_test.go b/coder-sdk/users_test.go index d84dc5c6..032f5508 100644 --- a/coder-sdk/users_test.go +++ b/coder-sdk/users_test.go @@ -68,9 +68,8 @@ func TestUserUpdatePassword(t *testing.T) { require.Equal(t, "/api/v0/users/me", r.URL.Path) expected := map[string]interface{}{ - "old_password": "vt9g9rxsptrq", - "password": "wmf39jw2f7pk", - "temporary_password": false, + "old_password": "vt9g9rxsptrq", + "password": "wmf39jw2f7pk", } var request map[string]interface{} err := json.NewDecoder(r.Body).Decode(&request)