Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 45 additions & 14 deletions coderd/httpmw/userparam.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import (

type userParamContextKey struct{}

const (
// userErrorMessage is a constant so that no information about the state
// of the queried user can be gained. We return the same error if the user
// does not exist, or if the input is just garbage.
userErrorMessage = "\"user\" must be an existing uuid or username"
)

// UserParam returns the user from the ExtractUserParam handler.
func UserParam(r *http.Request) database.User {
user, ok := r.Context().Value(userParamContextKey{}).(database.User)
Expand All @@ -27,32 +34,56 @@ func UserParam(r *http.Request) database.User {
func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
var userID uuid.UUID
if chi.URLParam(r, "user") == "me" {
userID = APIKey(r).UserID
var user database.User
var err error

// userQuery is either a uuid, a username, or 'me'
userQuery := chi.URLParam(r, "user")
if userQuery == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "\"user\" must be provided",
})
return
}

if userQuery == "me" {
user, err = db.GetUserByID(r.Context(), APIKey(r).UserID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get user: %s", err.Error()),
})
return
}
} else if userID, err := uuid.Parse(userQuery); err == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might reduce complexity if we just disallow getting by UUID entirely. I'm not sure it provides a ton of value anyways, because you need to get the user to get the ID. You could of course list users to do that, but then just use the username.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not inclined to drop the functionality because usernames might be able to be changed. UUIDs never will.

It's really nice to have a ID, as you can assume that will be constant no matter what.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough fair enough

// If the userQuery is a valid uuid
user, err = db.GetUserByID(r.Context(), userID)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: userErrorMessage,
})
return
}
} else {
var ok bool
userID, ok = parseUUID(rw, r, "user")
if !ok {
// Try as a username last
user, err = db.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Username: userQuery,
})
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: userErrorMessage,
})
return
}
}

apiKey := APIKey(r)
if apiKey.UserID != userID {
if apiKey.UserID != user.ID {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "getting non-personal users isn't supported yet",
})
return
}

user, err := db.GetUserByID(r.Context(), userID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get user: %s", err.Error()),
})
}

ctx := context.WithValue(r.Context(), userParamContextKey{}, user)
next.ServeHTTP(rw, r.WithContext(ctx))
})
Expand Down
4 changes: 3 additions & 1 deletion coderd/httpmw/userparam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ func TestUserParam(t *testing.T) {
})

user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.New(),
ID: uuid.New(),
Email: "admin@email.com",
Username: "admin",
})
require.NoError(t, err)

Expand Down
43 changes: 37 additions & 6 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,14 +431,45 @@ func TestPutUserSuspend(t *testing.T) {
})
}

func TestUserByName(t *testing.T) {
func TestGetUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
firstUser := coderdtest.CreateFirstUser(t, client)
user, err := client.User(context.Background(), codersdk.Me)

require.NoError(t, err)
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
t.Run("ByMe", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, nil)
firstUser := coderdtest.CreateFirstUser(t, client)

user, err := client.User(context.Background(), codersdk.Me)
require.NoError(t, err)
require.Equal(t, firstUser.UserID, user.ID)
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
})

t.Run("ByID", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, nil)
firstUser := coderdtest.CreateFirstUser(t, client)

user, err := client.User(context.Background(), firstUser.UserID)
require.NoError(t, err)
require.Equal(t, firstUser.UserID, user.ID)
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
})

t.Run("ByUsername", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, nil)
firstUser := coderdtest.CreateFirstUser(t, client)
exp, err := client.User(context.Background(), firstUser.UserID)
require.NoError(t, err)

user, err := client.UserByUsername(context.Background(), exp.Username)
require.NoError(t, err)
require.Equal(t, exp, user)
})
}

func TestGetUsers(t *testing.T) {
Expand Down
11 changes: 10 additions & 1 deletion codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,16 @@ func (c *Client) Logout(ctx context.Context) error {
// User returns a user for the ID provided.
// If the uuid is nil, the current user will be returned.
func (c *Client) User(ctx context.Context, id uuid.UUID) (User, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", uuidOrMe(id)), nil)
return c.userByIdentifier(ctx, uuidOrMe(id))
}

// UserByUsername returns a user for the username provided.
func (c *Client) UserByUsername(ctx context.Context, username string) (User, error) {
return c.userByIdentifier(ctx, username)
}

func (c *Client) userByIdentifier(ctx context.Context, ident string) (User, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", ident), nil)
if err != nil {
return User{}, err
}
Expand Down