Skip to content

Commit 08b01c0

Browse files
committed
Merge branch 'main' into statusbar/presleyp/1032
2 parents 7f6bbda + 57bb108 commit 08b01c0

32 files changed

+498
-96
lines changed

cli/cliui/agent.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"os"
8+
"os/signal"
79
"sync"
810
"time"
911

@@ -46,6 +48,23 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
4648
spin.Start()
4749
defer spin.Stop()
4850

51+
ctx, cancelFunc := context.WithCancel(ctx)
52+
defer cancelFunc()
53+
stopSpin := make(chan os.Signal, 1)
54+
signal.Notify(stopSpin, os.Interrupt)
55+
defer signal.Stop(stopSpin)
56+
go func() {
57+
select {
58+
case <-ctx.Done():
59+
return
60+
case <-stopSpin:
61+
}
62+
signal.Stop(stopSpin)
63+
spin.Stop()
64+
// nolint:revive
65+
os.Exit(1)
66+
}()
67+
4968
ticker := time.NewTicker(opts.FetchInterval)
5069
defer ticker.Stop()
5170
timer := time.NewTimer(opts.WarnInterval)

cli/login.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ func login() *cobra.Command {
117117
if err != nil {
118118
return xerrors.Errorf("specify password prompt: %w", err)
119119
}
120+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
121+
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
122+
Secret: true,
123+
Validate: func(s string) error {
124+
if s != password {
125+
return xerrors.Errorf("Passwords do not match")
126+
}
127+
return nil
128+
},
129+
})
130+
if err != nil {
131+
return xerrors.Errorf("confirm password prompt: %w", err)
132+
}
120133

121134
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
122135
Email: email,

cli/login_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func TestLogin(t *testing.T) {
4343
"username", "testuser",
4444
"email", "user@coder.com",
4545
"password", "password",
46+
"password", "password", // Confirm.
4647
}
4748
for i := 0; i < len(matches); i += 2 {
4849
match := matches[i]
@@ -54,6 +55,44 @@ func TestLogin(t *testing.T) {
5455
<-doneChan
5556
})
5657

58+
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
59+
t.Parallel()
60+
ctx, cancel := context.WithCancel(context.Background())
61+
defer cancel()
62+
client := coderdtest.New(t, nil)
63+
// The --force-tty flag is required on Windows, because the `isatty` library does not
64+
// accurately detect Windows ptys when they are not attached to a process:
65+
// https://github.com/mattn/go-isatty/issues/59
66+
doneChan := make(chan struct{})
67+
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
68+
pty := ptytest.New(t)
69+
root.SetIn(pty.Input())
70+
root.SetOut(pty.Output())
71+
go func() {
72+
defer close(doneChan)
73+
err := root.ExecuteContext(ctx)
74+
require.ErrorIs(t, err, context.Canceled)
75+
}()
76+
77+
matches := []string{
78+
"first user?", "yes",
79+
"username", "testuser",
80+
"email", "user@coder.com",
81+
"password", "mypass",
82+
"password", "wrongpass", // Confirm.
83+
}
84+
for i := 0; i < len(matches); i += 2 {
85+
match := matches[i]
86+
value := matches[i+1]
87+
pty.ExpectMatch(match)
88+
pty.WriteLine(value)
89+
}
90+
pty.ExpectMatch("Passwords do not match")
91+
pty.ExpectMatch("password") // Re-prompt password.
92+
cancel()
93+
<-doneChan
94+
})
95+
5796
t.Run("ExistingUserValidTokenTTY", func(t *testing.T) {
5897
t.Parallel()
5998
client := coderdtest.New(t, nil)

coderd/coderd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ func New(options *Options) (http.Handler, func()) {
240240
r.Get("/", api.userByName)
241241
r.Put("/profile", api.putUserProfile)
242242
r.Put("/suspend", api.putUserSuspend)
243+
r.Route("/password", func(r chi.Router) {
244+
r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole))
245+
r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate))
246+
})
243247
r.Get("/organizations", api.organizationsByUser)
244248
r.Post("/organizations", api.postOrganizationsByUser)
245249
// These roles apply to the site wide permissions.

coderd/coderdtest/coderdtest.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,21 +174,22 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
174174
return closer
175175
}
176176

177+
var FirstUserParams = codersdk.CreateFirstUserRequest{
178+
Email: "testuser@coder.com",
179+
Username: "testuser",
180+
Password: "testpass",
181+
OrganizationName: "testorg",
182+
}
183+
177184
// CreateFirstUser creates a user with preset credentials and authenticates
178185
// with the passed in codersdk client.
179186
func CreateFirstUser(t *testing.T, client *codersdk.Client) codersdk.CreateFirstUserResponse {
180-
req := codersdk.CreateFirstUserRequest{
181-
Email: "testuser@coder.com",
182-
Username: "testuser",
183-
Password: "testpass",
184-
OrganizationName: "testorg",
185-
}
186-
resp, err := client.CreateFirstUser(context.Background(), req)
187+
resp, err := client.CreateFirstUser(context.Background(), FirstUserParams)
187188
require.NoError(t, err)
188189

189190
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
190-
Email: req.Email,
191-
Password: req.Password,
191+
Email: FirstUserParams.Email,
192+
Password: FirstUserParams.Password,
192193
})
193194
require.NoError(t, err)
194195
client.SessionToken = login.SessionToken

coderd/database/databasefake/databasefake.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,21 @@ func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse
13141314
return database.User{}, sql.ErrNoRows
13151315
}
13161316

1317+
func (q *fakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error {
1318+
q.mutex.Lock()
1319+
defer q.mutex.Unlock()
1320+
1321+
for i, user := range q.users {
1322+
if user.ID != arg.ID {
1323+
continue
1324+
}
1325+
user.HashedPassword = arg.HashedPassword
1326+
q.users[i] = user
1327+
return nil
1328+
}
1329+
return sql.ErrNoRows
1330+
}
1331+
13171332
func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
13181333
q.mutex.Lock()
13191334
defer q.mutex.Unlock()

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/users.sql

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ WHERE
5959
id = @id
6060
RETURNING *;
6161

62+
-- name: UpdateUserHashedPassword :exec
63+
UPDATE
64+
users
65+
SET
66+
hashed_password = $2
67+
WHERE
68+
id = $1;
69+
6270
-- name: GetUsers :many
6371
SELECT
6472
*
@@ -133,4 +141,4 @@ FROM
133141
LEFT JOIN organization_members
134142
ON id = user_id
135143
WHERE
136-
id = @user_id;
144+
id = @user_id;

coderd/httpmw/userparam.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,6 @@ func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
7676
}
7777
}
7878

79-
apiKey := APIKey(r)
80-
if apiKey.UserID != user.ID {
81-
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
82-
Message: "getting non-personal users isn't supported yet",
83-
})
84-
return
85-
}
86-
8779
ctx := context.WithValue(r.Context(), userParamContextKey{}, user)
8880
next.ServeHTTP(rw, r.WithContext(ctx))
8981
})

coderd/rbac/object.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ var (
2424
Type: "user_role",
2525
}
2626

27+
ResourceUserPasswordRole = Object{
28+
Type: "user_password",
29+
}
30+
2731
// ResourceWildcard represents all resource types
2832
ResourceWildcard = Object{
2933
Type: WildcardSymbol,

coderd/users.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,36 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
360360
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
361361
}
362362

363+
func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {
364+
var (
365+
user = httpmw.UserParam(r)
366+
params codersdk.UpdateUserPasswordRequest
367+
)
368+
if !httpapi.Read(rw, r, &params) {
369+
return
370+
}
371+
372+
hashedPassword, err := userpassword.Hash(params.Password)
373+
if err != nil {
374+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
375+
Message: fmt.Sprintf("hash password: %s", err.Error()),
376+
})
377+
return
378+
}
379+
err = api.Database.UpdateUserHashedPassword(r.Context(), database.UpdateUserHashedPasswordParams{
380+
ID: user.ID,
381+
HashedPassword: []byte(hashedPassword),
382+
})
383+
if err != nil {
384+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
385+
Message: fmt.Sprintf("put user password: %s", err.Error()),
386+
})
387+
return
388+
}
389+
390+
httpapi.Write(rw, http.StatusNoContent, nil)
391+
}
392+
363393
func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) {
364394
user := httpmw.UserParam(r)
365395

@@ -577,7 +607,6 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
577607
}
578608

579609
// If the user doesn't exist, it will be a default struct.
580-
581610
equal, err := userpassword.Compare(string(user.HashedPassword), loginWithPassword.Password)
582611
if err != nil {
583612
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{

coderd/users_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,44 @@ func TestUpdateUserProfile(t *testing.T) {
287287
})
288288
}
289289

290+
func TestUpdateUserPassword(t *testing.T) {
291+
t.Parallel()
292+
293+
t.Run("MemberCantUpdateAdminPassword", func(t *testing.T) {
294+
t.Parallel()
295+
client := coderdtest.New(t, nil)
296+
admin := coderdtest.CreateFirstUser(t, client)
297+
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
298+
err := member.UpdateUserPassword(context.Background(), admin.UserID, codersdk.UpdateUserPasswordRequest{
299+
Password: "newpassword",
300+
})
301+
require.Error(t, err, "member should not be able to update admin password")
302+
})
303+
304+
t.Run("AdminCanUpdateMemberPassword", func(t *testing.T) {
305+
t.Parallel()
306+
client := coderdtest.New(t, nil)
307+
admin := coderdtest.CreateFirstUser(t, client)
308+
member, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
309+
Email: "coder@coder.com",
310+
Username: "coder",
311+
Password: "password",
312+
OrganizationID: admin.OrganizationID,
313+
})
314+
require.NoError(t, err, "create member")
315+
err = client.UpdateUserPassword(context.Background(), member.ID, codersdk.UpdateUserPasswordRequest{
316+
Password: "newpassword",
317+
})
318+
require.NoError(t, err, "admin should be able to update member password")
319+
// Check if the member can login using the new password
320+
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
321+
Email: "coder@coder.com",
322+
Password: "newpassword",
323+
})
324+
require.NoError(t, err, "member should login successfully with the new password")
325+
})
326+
}
327+
290328
func TestGrantRoles(t *testing.T) {
291329
t.Parallel()
292330
t.Run("UpdateIncorrectRoles", func(t *testing.T) {

codersdk/users.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ type UpdateUserProfileRequest struct {
7272
Username string `json:"username" validate:"required,username"`
7373
}
7474

75+
type UpdateUserPasswordRequest struct {
76+
Password string `json:"password" validate:"required"`
77+
}
78+
7579
type UpdateRoles struct {
7680
Roles []string `json:"roles" validate:"required"`
7781
}
@@ -181,6 +185,20 @@ func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error
181185
return user, json.NewDecoder(res.Body).Decode(&user)
182186
}
183187

188+
// UpdateUserPassword updates a user password.
189+
// It calls PUT /users/{user}/password
190+
func (c *Client) UpdateUserPassword(ctx context.Context, userID uuid.UUID, req UpdateUserPasswordRequest) error {
191+
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", uuidOrMe(userID)), req)
192+
if err != nil {
193+
return err
194+
}
195+
defer res.Body.Close()
196+
if res.StatusCode != http.StatusNoContent {
197+
return readBodyAsError(res)
198+
}
199+
return nil
200+
}
201+
184202
// UpdateUserRoles grants the userID the specified roles.
185203
// Include ALL roles the user has.
186204
func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req UpdateRoles) (User, error) {

docs/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=soc
1010

1111
Provision remote development environments with Terraform.
1212

13+
![Kubernetes workspace in Coder v2](./screenshot.png)
14+
1315
## Highlights
1416

1517
- Automate development environments for Linux, Windows, and macOS
@@ -100,6 +102,6 @@ Read the [contributing docs](./CONTRIBUTING.md).
100102

101103
<!--- Add your row by date (mm/dd/yyyy), most recent date at end of list --->
102104

103-
| Name | Start Date | First PR Date |Organization| GitHub User Link |
104-
| ------------- | :--------: | :-----------: |:----------:| ------------------------------: |
105-
| Mathias Fredriksson | 04/25/2022 | 04/25/2022 | [Coder](https://github.com/coder) | [mafredri](https://github.com/mafredri) |
105+
| Name | Start Date | First PR Date | Organization | GitHub User Link |
106+
| ------------------- | :--------: | :-----------: | :-------------------------------: | --------------------------------------: |
107+
| Mathias Fredriksson | 04/25/2022 | 04/25/2022 | [Coder](https://github.com/coder) | [mafredri](https://github.com/mafredri) |

docs/screenshot.png

520 KB
Loading

0 commit comments

Comments
 (0)