diff --git a/tenant/http_client_urm.go b/tenant/http_client_urm.go new file mode 100644 index 00000000000..25c4d2d1203 --- /dev/null +++ b/tenant/http_client_urm.go @@ -0,0 +1,127 @@ +package tenant + +import ( + "context" + "path" + + "github.com/influxdata/influxdb/v2" + "github.com/influxdata/influxdb/v2/pkg/httpc" +) + +type UserResourceMappingClient struct { + Client *httpc.Client +} + +// CreateUserResourceMapping will create a user resource mapping +func (s *UserResourceMappingClient) CreateUserResourceMapping(ctx context.Context, m *influxdb.UserResourceMapping) error { + if err := m.Validate(); err != nil { + return err + } + + urlPath := resourceIDPath(m.ResourceType, m.ResourceID, string(m.UserType)+"s") + return s.Client. + PostJSON(influxdb.User{ID: m.UserID}, urlPath). + DecodeJSON(m). + Do(ctx) +} + +// FindUserResourceMappings returns the user resource mappings +func (s *UserResourceMappingClient) FindUserResourceMappings(ctx context.Context, f influxdb.UserResourceMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.UserResourceMapping, int, error) { + var results resourceUsersResponse + err := s.Client. + Get(resourceIDPath(f.ResourceType, f.ResourceID, string(f.UserType)+"s")). + DecodeJSON(&results). + Do(ctx) + if err != nil { + return nil, 0, err + } + + urs := make([]*influxdb.UserResourceMapping, len(results.Users)) + for k, item := range results.Users { + urs[k] = &influxdb.UserResourceMapping{ + ResourceID: f.ResourceID, + ResourceType: f.ResourceType, + UserID: item.User.ID, + UserType: item.Role, + } + } + return urs, len(urs), nil +} + +// DeleteUserResourceMapping will delete user resource mapping based in criteria. +func (s *UserResourceMappingClient) DeleteUserResourceMapping(ctx context.Context, resourceID influxdb.ID, userID influxdb.ID) error { + urlPath := resourceIDUserPath(influxdb.OrgsResourceType, resourceID, influxdb.Member, userID) + return s.Client. + Delete(urlPath). + Do(ctx) +} + +// SpecificURMSvc returns a urm service with specific resource and user types. +// this will help us stay compatible with the existing service contract but also allow for urm deletes to go through the correct +// api +func (s *UserResourceMappingClient) SpecificURMSvc(rt influxdb.ResourceType, ut influxdb.UserType) *SpecificURMSvc { + return &SpecificURMSvc{ + Client: s.Client, + rt: rt, + ut: ut, + } +} + +// SpecificURMSvc is a URM client that speaks to a specific resource with a specified user type +type SpecificURMSvc struct { + Client *httpc.Client + rt influxdb.ResourceType + ut influxdb.UserType +} + +// FindUserResourceMappings returns the user resource mappings +func (s *SpecificURMSvc) FindUserResourceMappings(ctx context.Context, f influxdb.UserResourceMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.UserResourceMapping, int, error) { + var results resourceUsersResponse + err := s.Client. + Get(resourceIDPath(s.rt, f.ResourceID, string(s.ut)+"s")). + DecodeJSON(&results). + Do(ctx) + if err != nil { + return nil, 0, err + } + + urs := make([]*influxdb.UserResourceMapping, len(results.Users)) + for k, item := range results.Users { + urs[k] = &influxdb.UserResourceMapping{ + ResourceID: f.ResourceID, + ResourceType: f.ResourceType, + UserID: item.User.ID, + UserType: item.Role, + } + } + return urs, len(urs), nil +} + +// CreateUserResourceMapping will create a user resource mapping +func (s *SpecificURMSvc) CreateUserResourceMapping(ctx context.Context, m *influxdb.UserResourceMapping) error { + if err := m.Validate(); err != nil { + return err + } + + urlPath := resourceIDPath(s.rt, m.ResourceID, string(s.ut)+"s") + return s.Client. + PostJSON(influxdb.User{ID: m.UserID}, urlPath). + DecodeJSON(m). + Do(ctx) +} + +// DeleteUserResourceMapping will delete user resource mapping based in criteria. +func (s *SpecificURMSvc) DeleteUserResourceMapping(ctx context.Context, resourceID influxdb.ID, userID influxdb.ID) error { + urlPath := resourceIDUserPath(s.rt, resourceID, s.ut, userID) + return s.Client. + Delete(urlPath). + Do(ctx) +} + +func resourceIDPath(resourceType influxdb.ResourceType, resourceID influxdb.ID, p string) string { + return path.Join("/api/v2/", string(resourceType), resourceID.String(), p) +} + +func resourceIDUserPath(resourceType influxdb.ResourceType, resourceID influxdb.ID, userType influxdb.UserType, userID influxdb.ID) string { + return path.Join("/api/v2/", string(resourceType), resourceID.String(), string(userType)+"s", userID.String()) +} diff --git a/tenant/http_client_user.go b/tenant/http_client_user.go index a281dd1f974..e8571fb9d45 100644 --- a/tenant/http_client_user.go +++ b/tenant/http_client_user.go @@ -18,7 +18,7 @@ type UserClientService struct { // FindMe returns user information about the owner of the token func (s *UserClientService) FindMe(ctx context.Context, id influxdb.ID) (*influxdb.User, error) { - var res userResponse + var res UserResponse err := s.Client. Get(prefixMe). DecodeJSON(&res). @@ -31,7 +31,7 @@ func (s *UserClientService) FindMe(ctx context.Context, id influxdb.ID) (*influx // FindUserByID returns a single user by ID. func (s *UserClientService) FindUserByID(ctx context.Context, id influxdb.ID) (*influxdb.User, error) { - var res userResponse + var res UserResponse err := s.Client. Get(prefixUsers, id.String()). DecodeJSON(&res). @@ -105,7 +105,7 @@ func (s *UserClientService) CreateUser(ctx context.Context, u *influxdb.User) er // UpdateUser updates a single user with changeset. // Returns the new user state after update. func (s *UserClientService) UpdateUser(ctx context.Context, id influxdb.ID, upd influxdb.UserUpdate) (*influxdb.User, error) { - var res userResponse + var res UserResponse err := s.Client. PatchJSON(upd, prefixUsers, id.String()). DecodeJSON(&res). diff --git a/tenant/http_handler_urm.go b/tenant/http_handler_urm.go index 15c0cb8b3f7..9e883e7a901 100644 --- a/tenant/http_handler_urm.go +++ b/tenant/http_handler_urm.go @@ -231,13 +231,13 @@ func (h *urmHandler) decodeDeleteRequest(ctx context.Context, r *http.Request) ( type resourceUserResponse struct { Role influxdb.UserType `json:"role"` - *userResponse + *UserResponse } func newResourceUserResponse(u *influxdb.User, userType influxdb.UserType) *resourceUserResponse { return &resourceUserResponse{ Role: userType, - userResponse: newUserResponse(u), + UserResponse: newUserResponse(u), } } diff --git a/tenant/http_handler_urm_test.go b/tenant/http_handler_urm_test.go index 59341479b7f..0834235a5a3 100644 --- a/tenant/http_handler_urm_test.go +++ b/tenant/http_handler_urm_test.go @@ -13,8 +13,10 @@ import ( "github.com/go-chi/chi" "github.com/google/go-cmp/cmp" "github.com/influxdata/influxdb/v2" + ihttp "github.com/influxdata/influxdb/v2/http" "github.com/influxdata/influxdb/v2/mock" "github.com/influxdata/influxdb/v2/tenant" + itesting "github.com/influxdata/influxdb/v2/testing" "go.uber.org/zap/zaptest" ) @@ -369,3 +371,123 @@ func TestUserResourceMappingService_PostMembersHandler(t *testing.T) { } } } + +func TestUserResourceMappingService_Client(t *testing.T) { + type fields struct { + userService influxdb.UserService + userResourceMappingService influxdb.UserResourceMappingService + } + type args struct { + resourceID string + userType influxdb.UserType + user influxdb.User + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "post members", + fields: fields{ + userService: &mock.UserService{ + FindUserByIDFn: func(ctx context.Context, id influxdb.ID) (*influxdb.User, error) { + return &influxdb.User{ID: id, Name: fmt.Sprintf("user%s", id), Status: influxdb.Active}, nil + }, + }, + userResourceMappingService: &mock.UserResourceMappingService{ + CreateMappingFn: func(ctx context.Context, m *influxdb.UserResourceMapping) error { + return nil + }, + FindMappingsFn: func(ctx context.Context, f influxdb.UserResourceMappingFilter) ([]*influxdb.UserResourceMapping, int, error) { + return []*influxdb.UserResourceMapping{&influxdb.UserResourceMapping{}}, 1, nil + }, + }, + }, + args: args{ + resourceID: "0000000000000099", + user: influxdb.User{ + ID: 1, + Name: "user0000000000000001", + Status: influxdb.Active, + }, + userType: influxdb.Member, + }, + }, + + { + name: "post owners", + fields: fields{ + userService: &mock.UserService{ + FindUserByIDFn: func(ctx context.Context, id influxdb.ID) (*influxdb.User, error) { + return &influxdb.User{ID: id, Name: fmt.Sprintf("user%s", id), Status: influxdb.Active}, nil + }, + }, + userResourceMappingService: &mock.UserResourceMappingService{ + CreateMappingFn: func(ctx context.Context, m *influxdb.UserResourceMapping) error { + return nil + }, + FindMappingsFn: func(ctx context.Context, f influxdb.UserResourceMappingFilter) ([]*influxdb.UserResourceMapping, int, error) { + return []*influxdb.UserResourceMapping{&influxdb.UserResourceMapping{}}, 1, nil + }, + }, + }, + args: args{ + resourceID: "0000000000000099", + user: influxdb.User{ + ID: 2, + Name: "user0000000000000002", + Status: influxdb.Active, + }, + userType: influxdb.Owner, + }, + }, + } + + for _, tt := range tests { + resourceTypes := []influxdb.ResourceType{ + influxdb.BucketsResourceType, + influxdb.DashboardsResourceType, + influxdb.OrgsResourceType, + influxdb.SourcesResourceType, + influxdb.TasksResourceType, + influxdb.TelegrafsResourceType, + influxdb.UsersResourceType, + } + + for _, resourceType := range resourceTypes { + t.Run(tt.name+"_"+string(resourceType), func(t *testing.T) { + // create server + h := tenant.NewURMHandler(zaptest.NewLogger(t), resourceType, "id", tt.fields.userService, tt.fields.userResourceMappingService) + router := chi.NewRouter() + router.Mount(fmt.Sprintf("/api/v2/%s/{id}/members", resourceType), h) + router.Mount(fmt.Sprintf("/api/v2/%s/{id}/owners", resourceType), h) + s := httptest.NewServer(router) + defer s.Close() + ctx := context.Background() + + resourceID := itesting.MustIDBase16(tt.args.resourceID) + urm := &influxdb.UserResourceMapping{ResourceType: resourceType, ResourceID: resourceID, UserType: tt.args.userType, UserID: tt.args.user.ID} + + httpClient, err := ihttp.NewHTTPClient(s.URL, "", false) + if err != nil { + t.Fatal(err) + } + c := tenant.UserResourceMappingClient{Client: httpClient} + err = c.CreateUserResourceMapping(ctx, urm) + + if err != nil { + t.Fatal(err) + } + + _, n, err := c.FindUserResourceMappings(ctx, influxdb.UserResourceMappingFilter{ResourceID: resourceID, ResourceType: resourceType, UserType: tt.args.userType}) + if err != nil { + t.Fatal(err) + } + if n != 1 { + t.Fatalf("expected 1 urm to be created, got: %d", n) + } + }) + } + } +} diff --git a/tenant/http_server_onboarding.go b/tenant/http_server_onboarding.go index 029ce9432dd..022e83b5a55 100644 --- a/tenant/http_server_onboarding.go +++ b/tenant/http_server_onboarding.go @@ -110,7 +110,7 @@ func (h *OnboardHandler) handleOnboardRequest(w http.ResponseWriter, r *http.Req } type onboardingResponse struct { - User *userResponse `json:"user"` + User *UserResponse `json:"user"` Bucket *bucketResponse `json:"bucket"` Organization orgResponse `json:"org"` Auth *authResponse `json:"auth"` diff --git a/tenant/http_server_user.go b/tenant/http_server_user.go index 85b343938da..49ce656d616 100644 --- a/tenant/http_server_user.go +++ b/tenant/http_server_user.go @@ -351,7 +351,7 @@ func decodeDeleteUserRequest(ctx context.Context, r *http.Request) (*deleteUserR type usersResponse struct { Links map[string]string `json:"links"` - Users []*userResponse `json:"users"` + Users []*UserResponse `json:"users"` } func (us usersResponse) ToInfluxdb() []*influxdb.User { @@ -367,7 +367,7 @@ func newUsersResponse(users []*influxdb.User) *usersResponse { Links: map[string]string{ "self": "/api/v2/users", }, - Users: []*userResponse{}, + Users: []*UserResponse{}, } for _, user := range users { res.Users = append(res.Users, newUserResponse(user)) @@ -375,14 +375,14 @@ func newUsersResponse(users []*influxdb.User) *usersResponse { return &res } -// userResponse is the response of user -type userResponse struct { +// UserResponse is the response of user +type UserResponse struct { Links map[string]string `json:"links"` influxdb.User } -func newUserResponse(u *influxdb.User) *userResponse { - return &userResponse{ +func newUserResponse(u *influxdb.User) *UserResponse { + return &UserResponse{ Links: map[string]string{ "self": fmt.Sprintf("/api/v2/users/%s", u.ID), }, diff --git a/tenant/service.go b/tenant/service.go index 0000acf6d4a..a15b38e1ea3 100644 --- a/tenant/service.go +++ b/tenant/service.go @@ -67,7 +67,7 @@ func (ts *Service) NewOrgHTTPHandler(log *zap.Logger, secretSvc influxdb.SecretS } func (ts *Service) NewBucketHTTPHandler(log *zap.Logger, labelSvc influxdb.LabelService) *BucketHandler { - urmHandler := NewURMHandler(log.With(zap.String("handler", "urm")), influxdb.OrgsResourceType, "id", ts.UserService, NewAuthedURMService(ts.OrganizationService, ts.UserResourceMappingService)) + urmHandler := NewURMHandler(log.With(zap.String("handler", "urm")), influxdb.BucketsResourceType, "id", ts.UserService, NewAuthedURMService(ts.OrganizationService, ts.UserResourceMappingService)) labelHandler := label.NewHTTPEmbeddedHandler(log.With(zap.String("handler", "label")), influxdb.BucketsResourceType, labelSvc) return NewHTTPBucketHandler(log.With(zap.String("handler", "bucket")), NewAuthedBucketService(ts.BucketService), labelSvc, urmHandler, labelHandler) }