From c017da7087013e502fb8f2decd9df7d0e1465582 Mon Sep 17 00:00:00 2001 From: Todd Knight Date: Thu, 2 Jul 2020 08:17:52 -0700 Subject: [PATCH] Add ListUsers handler implementation. (#160) --- .../controller/handlers/users/user_service.go | 37 ++++++++- .../handlers/users/user_service_test.go | 80 +++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/internal/servers/controller/handlers/users/user_service.go b/internal/servers/controller/handlers/users/user_service.go index ec5830b245..f0a2c1415c 100644 --- a/internal/servers/controller/handlers/users/user_service.go +++ b/internal/servers/controller/handlers/users/user_service.go @@ -44,9 +44,16 @@ func NewService(repo func() (*iam.Repository, error)) (Service, error) { var _ pbs.UserServiceServer = Service{} -// CreateUser is not yet implemented but will implement the interface pbs.UserServiceServer. -func (s Service) ListUsers(context.Context, *pbs.ListUsersRequest) (*pbs.ListUsersResponse, error) { - return nil, status.Error(codes.Unimplemented, "List not enabled for this resource.") +// ListUsers implements the interface pbs.UserServiceServer. +func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs.ListUsersResponse, error) { + if err := validateListRequest(req); err != nil { + return nil, err + } + ul, err := s.listFromRepo(ctx, req.GetOrgId()) + if err != nil { + return nil, err + } + return &pbs.ListUsersResponse{Items: ul}, nil } // GetUsers implements the interface pbs.UserServiceServer. @@ -190,6 +197,22 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { return rows > 0, nil } +func (s Service) listFromRepo(ctx context.Context, orgId string) ([]*pb.User, error) { + repo, err := s.repoFn() + if err != nil { + return nil, err + } + ul, err := repo.ListUsers(ctx, orgId) + if err != nil { + return nil, err + } + var outUl []*pb.User + for _, u := range ul { + outUl = append(outUl, toProto(u)) + } + return outUl, nil +} + // toDbUpdateMask converts the wire format's FieldMask into a list of strings containing FieldMask paths used func toDbUpdateMask(paths []string) ([]string, error) { var dbPaths []string @@ -300,6 +323,14 @@ func validateDeleteRequest(req *pbs.DeleteUserRequest) error { return nil } +func validateListRequest(req *pbs.ListUsersRequest) error { + badFields := validateAncestors(req) + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields) + } + return nil +} + func validId(id, prefix string) bool { if !strings.HasPrefix(id, prefix) { return false diff --git a/internal/servers/controller/handlers/users/user_service_test.go b/internal/servers/controller/handlers/users/user_service_test.go index 5f1dea87b9..ebedfb4de4 100644 --- a/internal/servers/controller/handlers/users/user_service_test.go +++ b/internal/servers/controller/handlers/users/user_service_test.go @@ -12,6 +12,7 @@ import ( pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services" "github.com/hashicorp/watchtower/internal/iam" "github.com/hashicorp/watchtower/internal/servers/controller/handlers/users" + "github.com/hashicorp/watchtower/internal/types/scope" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -107,6 +108,85 @@ func TestGet(t *testing.T) { } } +func TestList(t *testing.T) { + assert, require := assert.New(t), require.New(t) + cleanup, conn, _ := db.TestSetup(t, "postgres") + t.Cleanup(func() { + if err := conn.Close(); err != nil { + t.Errorf("Error when closing gorm DB: %v", err) + } + if err := cleanup(); err != nil { + t.Errorf("Error when cleaning up TestSetup: %v", err) + } + }) + rw := db.New(conn) + wrap := db.TestWrapper(t) + repoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, wrap) + } + repo, err := repoFn() + require.NoError(err) + + oNoUsers, _ := iam.TestScopes(t, conn) + oWithUsers, _ := iam.TestScopes(t, conn) + + var wantUsers []*pb.User + for i := 0; i < 10; i++ { + newU, err := iam.NewUser(oWithUsers.GetPublicId()) + require.NoError(err) + u, err := repo.CreateUser(context.Background(), newU) + require.NoError(err) + wantUsers = append(wantUsers, &pb.User{ + Id: u.GetPublicId(), + CreatedTime: u.GetCreateTime().GetTimestamp(), + UpdatedTime: u.GetUpdateTime().GetTimestamp(), + }) + } + + cases := []struct { + name string + req *pbs.ListUsersRequest + res *pbs.ListUsersResponse + errCode codes.Code + }{ + { + name: "List Many Users", + req: &pbs.ListUsersRequest{OrgId: oWithUsers.GetPublicId()}, + res: &pbs.ListUsersResponse{Items: wantUsers}, + errCode: codes.OK, + }, + { + name: "List No Users", + req: &pbs.ListUsersRequest{OrgId: oNoUsers.GetPublicId()}, + res: &pbs.ListUsersResponse{}, + errCode: codes.OK, + }, + { + name: "Invalid Org Id", + req: &pbs.ListUsersRequest{OrgId: iam.UserPrefix + "_this is invalid"}, + res: nil, + errCode: codes.InvalidArgument, + }, + // TODO: When an org doesn't exist, we should return a 404 instead of an empty list. + { + name: "Unfound Org", + req: &pbs.ListUsersRequest{OrgId: scope.Organization.Prefix() + "_DoesntExis"}, + res: &pbs.ListUsersResponse{}, + errCode: codes.OK, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s, err := users.NewService(repoFn) + require.NoError(err, "Couldn't create new user service.") + + got, gErr := s.ListUsers(context.Background(), tc.req) + assert.Equal(tc.errCode, status.Code(gErr), "ListUsers(%+v) got error %v, wanted %v", tc.req, gErr, tc.errCode) + assert.True(proto.Equal(got, tc.res), "ListUsers(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + func TestDelete(t *testing.T) { require := require.New(t) u, repo := createDefaultUserAndRepo(t)