diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63b0ce8cf..b69ebb2a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: ports: - "4000:80" spicedb: - image: quay.io/authzed/spicedb:v1.0.0 + image: authzed/spicedb:v1.32.0 ports: - "8080:8080" - "50051:50051" diff --git a/Makefile b/Makefile index 6e8d07d99..229f83af4 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ GOVERSION := $(shell go version | cut -d ' ' -f 3 | cut -d '.' -f 2) .PHONY: build check fmt lint test test-race vet test-cover-html help install proto .DEFAULT_GOAL := build -PROTON_COMMIT := "e8a584e192f23f999844b027d17bd738c3981973" +PROTON_COMMIT := "6ee59f2d0cbeedf1d5fe48adee5e5e41f54f081e" install: @echo "Clean up imports..." diff --git a/cmd/serve.go b/cmd/serve.go index c006e3ba4..042d61c80 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -243,7 +243,7 @@ func BuildAPIDependencies( serviceDataRepository := postgres.NewServiceDataRepository(dbc) serviceDataService := servicedata.NewService(logger, serviceDataRepository, resourceService, relationService, projectService, userService, activityService) - relationAdapter := adapter.NewRelation(groupService, userService, relationService) + relationAdapter := adapter.NewRelation(groupService, userService, relationService, roleService) dependencies := api.Deps{ OrgService: organizationService, diff --git a/core/group/filter.go b/core/group/filter.go index e947d0bd8..6d371dd95 100644 --- a/core/group/filter.go +++ b/core/group/filter.go @@ -1,5 +1,7 @@ package group type Filter struct { - OrganizationID string + OrganizationID string + ProjectID string + ServicedataKeyResourceIDs []string } diff --git a/core/servicedata/service.go b/core/servicedata/service.go index e56f60809..35463ee06 100644 --- a/core/servicedata/service.go +++ b/core/servicedata/service.go @@ -15,6 +15,7 @@ import ( "github.com/goto/shield/core/user" "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/db" + "github.com/goto/shield/pkg/errors" ) const ( @@ -75,7 +76,7 @@ func NewService(logger log.Logger, repository Repository, resourceService Resour func (s Service) CreateKey(ctx context.Context, key Key) (Key, error) { // check if key contains ':' - if key.Key == "" { + if key.Name == "" { return Key{}, ErrInvalidDetail } @@ -94,7 +95,7 @@ func (s Service) CreateKey(ctx context.Context, key Key) (Key, error) { key.ProjectSlug = prj.Slug // create URN - key.URN = key.CreateURN() + key.URN = CreateURN(key.ProjectSlug, key.Name) // Transaction for postgres repository // TODO find way to use transaction for spicedb @@ -159,8 +160,12 @@ func (s Service) CreateKey(ctx context.Context, key Key) (Key, error) { return createdServiceDataKey, nil } +func (s Service) GetKeyByURN(ctx context.Context, urn string) (Key, error) { + return s.repository.GetKeyByURN(ctx, urn) +} + func (s Service) Upsert(ctx context.Context, sd ServiceData) (ServiceData, error) { - if sd.Key.Key == "" { + if sd.Key.Name == "" { return ServiceData{}, ErrInvalidDetail } @@ -175,7 +180,7 @@ func (s Service) Upsert(ctx context.Context, sd ServiceData) (ServiceData, error } sd.Key.ProjectSlug = prj.Slug - sd.Key.URN = sd.Key.CreateURN() + sd.Key.URN = CreateURN(sd.Key.ProjectSlug, sd.Key.Name) sd.Key, err = s.repository.GetKeyByURN(ctx, sd.Key.URN) if err != nil { @@ -188,7 +193,7 @@ func (s Service) Upsert(ctx context.Context, sd ServiceData) (ServiceData, error return ServiceData{}, err } if !permission { - return ServiceData{}, user.ErrInvalidEmail + return ServiceData{}, errors.ErrForbidden } returnedServiceData, err := s.repository.Upsert(ctx, sd) diff --git a/core/servicedata/service_test.go b/core/servicedata/service_test.go index af1cecbe8..670949f2d 100644 --- a/core/servicedata/service_test.go +++ b/core/servicedata/service_test.go @@ -14,7 +14,9 @@ import ( "github.com/goto/shield/core/servicedata/mocks" "github.com/goto/shield/core/user" "github.com/goto/shield/internal/schema" + errorsPkg "github.com/goto/shield/pkg/errors" "github.com/goto/shield/pkg/logger" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -30,21 +32,21 @@ var ( testProjectSlug = "test-project-slug" testKey = servicedata.Key{ ProjectID: "test-project-slug", - Key: "test-key", + Name: "test-key", Description: "test key no 01", } testCreateKey = servicedata.Key{ URN: "test-project-slug:servicedata_key:test-key", ProjectID: testProjectID, ProjectSlug: testProjectSlug, - Key: "test-key", + Name: "test-key", Description: "test key no 01", ResourceID: testResourceID, } testCreatedKey = servicedata.Key{ URN: "test-project-slug:servicedata_key:test-key", ProjectID: testProjectID, - Key: "test-key", + Name: "test-key", Description: "test key no 01", ResourceID: testResourceID, } @@ -131,7 +133,7 @@ func TestService_CreateKey(t *testing.T) { name: "CreateKeyEmpty", key: servicedata.Key{ ProjectID: testKey.ProjectID, - Key: "", + Name: "", Description: testKey.Description, }, setup: func(t *testing.T) *servicedata.Service { @@ -183,7 +185,7 @@ func TestService_CreateKey(t *testing.T) { name: "CreateKeyInvalidProjectID", key: servicedata.Key{ ProjectID: "invalid-test-project-slug", - Key: testKey.Key, + Name: testKey.Name, Description: testKey.Description, }, email: "jane.doe@gotocompany.com", @@ -366,7 +368,7 @@ func TestService_Upsert(t *testing.T) { name: "UpsertKeyEmpty", data: servicedata.ServiceData{ Key: servicedata.Key{ - Key: "", + Name: "", }, }, setup: func(t *testing.T) *servicedata.Service { @@ -402,7 +404,7 @@ func TestService_Upsert(t *testing.T) { name: "UpsertInvalidProjectID", data: servicedata.ServiceData{ Key: servicedata.Key{ - Key: testKey.Key, + Name: testKey.Name, ProjectID: "invalid-test-project-slug", }, }, @@ -480,7 +482,7 @@ func TestService_Upsert(t *testing.T) { testResourceID, action.Action{ID: "edit"}).Return(false, nil) return servicedata.NewService(testLogger, repository, resourceService, relationService, projectService, userService, activityService) }, - wantErr: user.ErrInvalidEmail, + wantErr: errorsPkg.ErrForbidden, }, { name: "UpsertErr", diff --git a/core/servicedata/servicedata.go b/core/servicedata/servicedata.go index 3ccd322e8..e193dd31f 100644 --- a/core/servicedata/servicedata.go +++ b/core/servicedata/servicedata.go @@ -26,7 +26,7 @@ type Key struct { URN string ProjectID string ProjectSlug string - Key string + Name string Description string ResourceID string } @@ -36,7 +36,7 @@ type ServiceData struct { NamespaceID string EntityID string Key Key - Value string + Value any } type KeyLogData struct { @@ -55,8 +55,8 @@ type Filter struct { Project string } -func (key Key) CreateURN() string { - return fmt.Sprintf("%s:servicedata_key:%s", key.ProjectSlug, key.Key) +func CreateURN(projectSlug, keyName string) string { + return fmt.Sprintf("%s:servicedata_key:%s", projectSlug, keyName) } func (key Key) ToKeyLogData() KeyLogData { @@ -64,7 +64,7 @@ func (key Key) ToKeyLogData() KeyLogData { Entity: auditEntityServiceDataKey, URN: key.URN, ProjectSlug: key.ProjectSlug, - Key: key.Key, + Key: key.Name, Description: key.Description, } } diff --git a/core/user/filter.go b/core/user/filter.go index c7b61aa81..5093f4a12 100644 --- a/core/user/filter.go +++ b/core/user/filter.go @@ -1,7 +1,9 @@ package user type Filter struct { - Limit int32 - Page int32 - Keyword string + Limit int32 + Page int32 + Keyword string + ProjectID string + ServiceDataKeyResourceIds []string } diff --git a/internal/adapter/relation.go b/internal/adapter/relation.go index c222b9fb1..db3dd430c 100644 --- a/internal/adapter/relation.go +++ b/internal/adapter/relation.go @@ -3,29 +3,36 @@ package adapter import ( "context" "fmt" + "slices" "github.com/goto/shield/core/group" "github.com/goto/shield/core/relation" + "github.com/goto/shield/core/role" "github.com/goto/shield/core/user" "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/uuid" ) +const WILDCARD = "*" + type Relation struct { groupService *group.Service userService *user.Service relationService *relation.Service + roleService *role.Service } func NewRelation( groupService *group.Service, userService *user.Service, relationService *relation.Service, + roleService *role.Service, ) *Relation { return &Relation{ groupService: groupService, userService: userService, relationService: relationService, + roleService: roleService, } } @@ -36,7 +43,12 @@ func (a Relation) TransformRelation(ctx context.Context, rlt relation.RelationV2 if rel.Subject.Namespace == schema.UserPrincipal || rel.Subject.Namespace == "user" { userID := rel.Subject.ID - if !uuid.IsValid(userID) { + if userID == WILDCARD { + err := a.isWildCardAllowed(ctx, rel) + if err != nil { + return relation.RelationV2{}, err + } + } else if !uuid.IsValid(userID) { fetchedUser, err := a.userService.GetByEmail(ctx, rel.Subject.ID) if err != nil { return relation.RelationV2{}, fmt.Errorf("%w: %s", relation.ErrFetchingUser, err.Error()) @@ -79,3 +91,16 @@ func (a Relation) TransformRelation(ctx context.Context, rlt relation.RelationV2 return rel, nil } + +func (a Relation) isWildCardAllowed(ctx context.Context, rlt relation.RelationV2) error { + roleID := rlt.Object.NamespaceID + ":" + rlt.Subject.RoleID + role, err := a.roleService.Get(ctx, roleID) + if err != nil { + return fmt.Errorf("error fetching role: %s", err.Error()) + } + if !slices.Contains(role.Types, schema.UserPrincipalWildcard) { + return fmt.Errorf("%s does not allow wildcard for subject %s", rlt.Object.NamespaceID, rlt.Subject.Namespace) + } + + return nil +} diff --git a/internal/api/v1beta1/group.go b/internal/api/v1beta1/group.go index 1eecfdbdd..056b54d5d 100644 --- a/internal/api/v1beta1/group.go +++ b/internal/api/v1beta1/group.go @@ -2,18 +2,26 @@ package v1beta1 import ( "context" + "fmt" "strings" "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/errors" + errorsPkg "github.com/goto/shield/pkg/errors" "github.com/goto/shield/pkg/metadata" "github.com/goto/shield/pkg/str" "github.com/goto/shield/pkg/uuid" + "golang.org/x/exp/maps" grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/goto/shield/core/action" "github.com/goto/shield/core/group" + "github.com/goto/shield/core/namespace" "github.com/goto/shield/core/organization" + "github.com/goto/shield/core/project" + "github.com/goto/shield/core/relation" + "github.com/goto/shield/core/servicedata" "github.com/goto/shield/core/user" shieldv1beta1 "github.com/goto/shield/proto/v1beta1" @@ -32,15 +40,49 @@ type GroupService interface { ListGroupRelations(ctx context.Context, objectId, subjectType, role string) ([]user.User, []group.Group, map[string][]string, map[string][]string, error) } -var grpcGroupNotFoundErr = status.Errorf(codes.NotFound, "group doesn't exist") +var ( + grpcGroupNotFoundErr = status.Errorf(codes.NotFound, "group doesn't exist") + grpcInvalidOrgIDErr = status.Errorf(codes.InvalidArgument, "ordIs is not valid uuid") +) func (h Handler) ListGroups(ctx context.Context, request *shieldv1beta1.ListGroupsRequest) (*shieldv1beta1.ListGroupsResponse, error) { logger := grpczap.Extract(ctx) + if request.GetOrgId() != "" { + if !uuid.IsValid(request.GetOrgId()) { + return nil, grpcInvalidOrgIDErr + } + + _, err := h.orgService.Get(ctx, request.GetOrgId()) + if err != nil { + return &shieldv1beta1.ListGroupsResponse{Groups: nil}, nil + } + } + var groups []*shieldv1beta1.Group + currentUser, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) + return nil, grpcUnauthenticated + } + + servicedataKeyResourceIds, err := h.relationService.LookupResources(ctx, schema.ServiceDataKeyNamespace, schema.ViewPermission, schema.UserPrincipal, currentUser.ID) + if err != nil { + logger.Error(err.Error()) + return nil, grpcInternalServerError + } + + prj, err := h.projectService.Get(ctx, h.serviceDataConfig.DefaultServiceDataProject) + if err != nil { + logger.Error(err.Error()) + return nil, grpcInternalServerError + } + groupList, err := h.groupService.List(ctx, group.Filter{ - OrganizationID: request.GetOrgId(), + OrganizationID: request.GetOrgId(), + ProjectID: prj.ID, + ServicedataKeyResourceIDs: servicedataKeyResourceIds, }) if err != nil { logger.Error(err.Error()) @@ -67,17 +109,40 @@ func (h Handler) CreateGroup(ctx context.Context, request *shieldv1beta1.CreateG return nil, grpcBadBodyError } + currentUser, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) + return nil, grpcUnauthenticated + } + + // TODO: change this metaDataMap, err := metadata.Build(request.GetBody().GetMetadata().AsMap()) if err != nil { logger.Error(err.Error()) return nil, grpcBadBodyError } + for k := range metaDataMap { + urn := servicedata.CreateURN(h.serviceDataConfig.DefaultServiceDataProject, k) + key, err := h.serviceDataService.GetKeyByURN(ctx, urn) + if err != nil { + return nil, err + } + + permission, err := h.relationService.CheckPermission(ctx, currentUser, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, key.ResourceID, action.Action{ID: schema.EditPermission}) + if err != nil { + return nil, err + } + if !permission { + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + } + } + grp := group.Group{ Name: request.GetBody().GetName(), Slug: request.GetBody().GetSlug(), OrganizationID: request.GetBody().GetOrgId(), - Metadata: metaDataMap, + Metadata: nil, } if strings.TrimSpace(grp.Slug) == "" { @@ -100,26 +165,55 @@ func (h Handler) CreateGroup(ctx context.Context, request *shieldv1beta1.CreateG } } - metaData, err := newGroup.Metadata.ToStructPB() + serviceDataMap := map[string]any{} + for k, v := range metaDataMap { + serviceDataResp, err := h.serviceDataService.Upsert(ctx, servicedata.ServiceData{ + EntityID: newGroup.ID, + NamespaceID: groupNamespaceID, + Key: servicedata.Key{ + Name: k, + ProjectID: h.serviceDataConfig.DefaultServiceDataProject, + }, + Value: v, + }) + if err != nil { + logger.Error(err.Error()) + + switch { + case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): + return nil, grpcUnauthenticated + case errors.Is(err, project.ErrNotExist), errors.Is(err, servicedata.ErrInvalidDetail), + errors.Is(err, relation.ErrInvalidDetail), errors.Is(err, servicedata.ErrNotExist): + return nil, grpcBadBodyError + case errors.Is(err, errorsPkg.ErrForbidden): + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + default: + return nil, grpcInternalServerError + } + } + serviceDataMap[serviceDataResp.Key.Name] = serviceDataResp.Value + } + + newGroup.Metadata = metaDataMap + + groupPB, err := transformGroupToPB(newGroup) if err != nil { logger.Error(err.Error()) - return nil, grpcInternalServerError + return nil, ErrInternalServer } - return &shieldv1beta1.CreateGroupResponse{Group: &shieldv1beta1.Group{ - Id: newGroup.ID, - Name: newGroup.Name, - Slug: newGroup.Slug, - OrgId: newGroup.OrganizationID, - Metadata: metaData, - CreatedAt: timestamppb.New(newGroup.CreatedAt), - UpdatedAt: timestamppb.New(newGroup.UpdatedAt), - }}, nil + return &shieldv1beta1.CreateGroupResponse{Group: &groupPB}, nil } func (h Handler) GetGroup(ctx context.Context, request *shieldv1beta1.GetGroupRequest) (*shieldv1beta1.GetGroupResponse, error) { logger := grpczap.Extract(ctx) + _, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) + return nil, grpcUnauthenticated + } + fetchedGroup, err := h.groupService.Get(ctx, request.GetId()) if err != nil { logger.Error(err.Error()) @@ -131,6 +225,31 @@ func (h Handler) GetGroup(ctx context.Context, request *shieldv1beta1.GetGroupRe } } + filter := servicedata.Filter{ + ID: fetchedGroup.ID, + Namespace: groupNamespaceID, + Entities: maps.Values(map[string]string{ + "group": groupNamespaceID, + }), + } + + groupSD, err := h.serviceDataService.Get(ctx, filter) + if err != nil { + logger.Error(err.Error()) + switch { + case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): + break + default: + return nil, grpcInternalServerError + } + } else { + metadata := map[string]any{} + for _, sd := range groupSD { + metadata[sd.Key.Name] = sd.Value + } + fetchedGroup.Metadata = metadata + } + groupPB, err := transformGroupToPB(fetchedGroup) if err != nil { logger.Error(err.Error()) @@ -147,11 +266,34 @@ func (h Handler) UpdateGroup(ctx context.Context, request *shieldv1beta1.UpdateG return nil, grpcBadBodyError } + currentUser, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) + return nil, grpcUnauthenticated + } + + // TODO: change this implementation metaDataMap, err := metadata.Build(request.GetBody().GetMetadata().AsMap()) if err != nil { return nil, grpcBadBodyError } + for k := range metaDataMap { + urn := servicedata.CreateURN(h.serviceDataConfig.DefaultServiceDataProject, k) + key, err := h.serviceDataService.GetKeyByURN(ctx, urn) + if err != nil { + return nil, err + } + + permission, err := h.relationService.CheckPermission(ctx, currentUser, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, key.ResourceID, action.Action{ID: schema.EditPermission}) + if err != nil { + return nil, err + } + if !permission { + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + } + } + var updatedGroup group.Group if uuid.IsValid(request.GetId()) { updatedGroup, err = h.groupService.Update(ctx, group.Group{ @@ -159,14 +301,14 @@ func (h Handler) UpdateGroup(ctx context.Context, request *shieldv1beta1.UpdateG Name: request.GetBody().GetName(), Slug: request.GetBody().GetSlug(), OrganizationID: request.GetBody().GetOrgId(), - Metadata: metaDataMap, + Metadata: nil, }) } else { updatedGroup, err = h.groupService.Update(ctx, group.Group{ Name: request.GetBody().GetName(), Slug: request.GetId(), OrganizationID: request.GetBody().GetOrgId(), - Metadata: metaDataMap, + Metadata: nil, }) } if err != nil { @@ -190,6 +332,38 @@ func (h Handler) UpdateGroup(ctx context.Context, request *shieldv1beta1.UpdateG } } + serviceDataMap := map[string]any{} + for k, v := range metaDataMap { + serviceDataResp, err := h.serviceDataService.Upsert(ctx, servicedata.ServiceData{ + EntityID: updatedGroup.ID, + NamespaceID: groupNamespaceID, + Key: servicedata.Key{ + Name: k, + ProjectID: h.serviceDataConfig.DefaultServiceDataProject, + }, + Value: v, + }) + if err != nil { + logger.Error(err.Error()) + + switch { + case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): + return nil, grpcUnauthenticated + case errors.Is(err, project.ErrNotExist), errors.Is(err, servicedata.ErrInvalidDetail), + errors.Is(err, relation.ErrInvalidDetail), errors.Is(err, servicedata.ErrNotExist): + return nil, grpcBadBodyError + case errors.Is(err, errorsPkg.ErrForbidden): + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + default: + return nil, grpcInternalServerError + } + } + serviceDataMap[serviceDataResp.Key.Name] = serviceDataResp.Value + } + + // Note: this would return only the keys that are updated in the current request + updatedGroup.Metadata = metaDataMap + groupPB, err := transformGroupToPB(updatedGroup) if err != nil { return nil, grpcInternalServerError diff --git a/internal/api/v1beta1/group_test.go b/internal/api/v1beta1/group_test.go index 5a39922a5..2a98e97d9 100644 --- a/internal/api/v1beta1/group_test.go +++ b/internal/api/v1beta1/group_test.go @@ -5,8 +5,12 @@ import ( "testing" "time" + "github.com/goto/shield/core/action" "github.com/goto/shield/core/group" + "github.com/goto/shield/core/namespace" "github.com/goto/shield/core/organization" + "github.com/goto/shield/core/project" + "github.com/goto/shield/core/servicedata" "github.com/goto/shield/core/user" "github.com/goto/shield/internal/api/v1beta1/mocks" "github.com/goto/shield/internal/schema" @@ -14,6 +18,7 @@ import ( "github.com/goto/shield/pkg/metadata" "github.com/goto/shield/pkg/uuid" shieldv1beta1 "github.com/goto/shield/proto/v1beta1" + "golang.org/x/exp/maps" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -104,32 +109,25 @@ func TestHandler_ListGroups(t *testing.T) { randomID := uuid.NewString() tests := []struct { name string - setup func(gs *mocks.GroupService) + setup func(gs *mocks.GroupService, os *mocks.OrganizationService, ps *mocks.ProjectService, us *mocks.UserService, rs *mocks.RelationService) request *shieldv1beta1.ListGroupsRequest want *shieldv1beta1.ListGroupsResponse wantErr error }{ { name: "should return empty groups if query param org_id is not uuid", - setup: func(gs *mocks.GroupService) { - gs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), group.Filter{ - OrganizationID: "some-id", - }).Return([]group.Group{}, nil) + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, ps *mocks.ProjectService, us *mocks.UserService, rs *mocks.RelationService) { }, request: &shieldv1beta1.ListGroupsRequest{ OrgId: "some-id", }, - want: &shieldv1beta1.ListGroupsResponse{ - Groups: nil, - }, - wantErr: nil, + want: nil, + wantErr: grpcInvalidOrgIDErr, }, { name: "should return empty groups if query param org_id is not exist", - setup: func(gs *mocks.GroupService) { - gs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), group.Filter{ - OrganizationID: randomID, - }).Return([]group.Group{}, nil) + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, ps *mocks.ProjectService, us *mocks.UserService, rs *mocks.RelationService) { + os.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), randomID).Return(organization.Organization{}, errors.New("some error")) }, request: &shieldv1beta1.ListGroupsRequest{ OrgId: randomID, @@ -141,12 +139,29 @@ func TestHandler_ListGroups(t *testing.T) { }, { name: "should return all groups if no query param filter exist", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, ps *mocks.ProjectService, us *mocks.UserService, rs *mocks.RelationService) { var testGroupList []group.Group for _, u := range testGroupMap { testGroupList = append(testGroupList, u) } - gs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), group.Filter{}).Return(testGroupList, nil) + os.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "some-id").Return(organization.Organization{}, errors.New("some error")) + + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) + + rs.EXPECT().LookupResources(mock.AnythingOfType("context.todoCtx"), schema.ServiceDataKeyNamespace, schema.ViewPermission, schema.UserPrincipal, "083a77a2-ab14-40d2-a06d-f6d9f80c6378").Return([]string{}, nil) + + ps.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "").Return(project.Project{ + Name: "system", + Slug: "system", + ID: "78849300-1146-4875-9cce-67ba353ed97e", + }, nil) + + gs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), group.Filter{ + ProjectID: "78849300-1146-4875-9cce-67ba353ed97e", + ServicedataKeyResourceIDs: []string{}, + }).Return(testGroupList, nil) }, request: &shieldv1beta1.ListGroupsRequest{}, want: &shieldv1beta1.ListGroupsResponse{ @@ -170,14 +185,35 @@ func TestHandler_ListGroups(t *testing.T) { }, { name: "should return filtered groups if query param org_id exist", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, ps *mocks.ProjectService, us *mocks.UserService, rs *mocks.RelationService) { var testGroupList []group.Group for _, u := range testGroupMap { testGroupList = append(testGroupList, u) } + + os.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "9f256f86-31a3-11ec-8d3d-0242ac130003").Return(organization.Organization{ + ID: "9f256f86-31a3-11ec-8d3d-0242ac130003", + }, nil) + + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) + + rs.EXPECT().LookupResources(mock.AnythingOfType("context.todoCtx"), schema.ServiceDataKeyNamespace, schema.ViewPermission, schema.UserPrincipal, "083a77a2-ab14-40d2-a06d-f6d9f80c6378").Return([]string{}, nil) + + ps.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "").Return(project.Project{ + Name: "system", + Slug: "system", + ID: "78849300-1146-4875-9cce-67ba353ed97e", + }, nil) + gs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), group.Filter{ - OrganizationID: "9f256f86-31a3-11ec-8d3d-0242ac130003", + OrganizationID: "9f256f86-31a3-11ec-8d3d-0242ac130003", + ProjectID: "78849300-1146-4875-9cce-67ba353ed97e", + ServicedataKeyResourceIDs: []string{}, }).Return(testGroupList, nil) + + gs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), group.Filter{}).Return(testGroupList, nil) }, request: &shieldv1beta1.ListGroupsRequest{ OrgId: "9f256f86-31a3-11ec-8d3d-0242ac130003", @@ -205,11 +241,19 @@ func TestHandler_ListGroups(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + mockProjectSvc := new(mocks.ProjectService) + mockUserSvc := new(mocks.UserService) + mockRelationSvc := new(mocks.RelationService) if tt.setup != nil { - tt.setup(mockGroupSvc) + tt.setup(mockGroupSvc, mockOrgSvc, mockProjectSvc, mockUserSvc, mockRelationSvc) } h := Handler{ - groupService: mockGroupSvc, + groupService: mockGroupSvc, + orgService: mockOrgSvc, + projectService: mockProjectSvc, + userService: mockUserSvc, + relationService: mockRelationSvc, } got, err := h.ListGroups(context.TODO(), tt.request) assert.EqualValues(t, got, tt.want) @@ -224,20 +268,23 @@ func TestHandler_CreateGroup(t *testing.T) { someGroupID := uuid.NewString() tests := []struct { name string - setup func(ctx context.Context, gs *mocks.GroupService) context.Context + setup func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context request *shieldv1beta1.CreateGroupRequest want *shieldv1beta1.CreateGroupResponse wantErr error }{ { name: "should return unauthenticated error if auth email in context is empty and group service return invalid user email", - setup: func(ctx context.Context, gs *mocks.GroupService) context.Context { + setup: func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) gs.EXPECT().Create(mock.AnythingOfType("context.todoCtx"), group.Group{ Name: "some group", Slug: "some-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, user.ErrInvalidEmail) return ctx }, @@ -251,15 +298,19 @@ func TestHandler_CreateGroup(t *testing.T) { }, { name: "should return internal error if group service return some error", - setup: func(ctx context.Context, gs *mocks.GroupService) context.Context { + setup: func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) gs.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), group.Group{ Name: "some group", Slug: "some-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, errors.New("some error")) - return user.SetContextWithEmail(ctx, email) + return ctx }, request: &shieldv1beta1.CreateGroupRequest{Body: &shieldv1beta1.GroupRequestBody{ Name: "some group", @@ -271,15 +322,19 @@ func TestHandler_CreateGroup(t *testing.T) { }, { name: "should return already exist error if group service return error conflict", - setup: func(ctx context.Context, gs *mocks.GroupService) context.Context { + setup: func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) gs.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), group.Group{ Name: "some group", Slug: "some-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrConflict) - return user.SetContextWithEmail(ctx, email) + return ctx }, request: &shieldv1beta1.CreateGroupRequest{Body: &shieldv1beta1.GroupRequestBody{ Name: "some group", @@ -292,14 +347,18 @@ func TestHandler_CreateGroup(t *testing.T) { }, { name: "should return bad request error if name empty", - setup: func(ctx context.Context, gs *mocks.GroupService) context.Context { + setup: func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) gs.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), group.Group{ Slug: "some-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrInvalidDetail) - return user.SetContextWithEmail(ctx, email) + return ctx }, request: &shieldv1beta1.CreateGroupRequest{Body: &shieldv1beta1.GroupRequestBody{ Slug: "some-group", @@ -311,14 +370,18 @@ func TestHandler_CreateGroup(t *testing.T) { }, { name: "should return bad request error if org id is not uuid", - setup: func(ctx context.Context, gs *mocks.GroupService) context.Context { + setup: func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) gs.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), group.Group{ Name: "some group", Slug: "some-group", OrganizationID: "some-org-id", - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, organization.ErrInvalidUUID) - return user.SetContextWithEmail(ctx, email) + return ctx }, request: &shieldv1beta1.CreateGroupRequest{Body: &shieldv1beta1.GroupRequestBody{ Name: "some group", @@ -331,15 +394,19 @@ func TestHandler_CreateGroup(t *testing.T) { }, { name: "should return bad request error if org id not exist", - setup: func(ctx context.Context, gs *mocks.GroupService) context.Context { + setup: func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) gs.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), group.Group{ Name: "some group", Slug: "some-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, organization.ErrNotExist) - return user.SetContextWithEmail(ctx, email) + return ctx }, request: &shieldv1beta1.CreateGroupRequest{Body: &shieldv1beta1.GroupRequestBody{ Name: "some group", @@ -358,13 +425,24 @@ func TestHandler_CreateGroup(t *testing.T) { }, { name: "should return success if group service return nil", - setup: func(ctx context.Context, gs *mocks.GroupService) context.Context { + setup: func(ctx context.Context, gs *mocks.GroupService, us *mocks.UserService, rs *mocks.RelationService, sds *mocks.ServiceDataService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) + sds.EXPECT().GetKeyByURN(ctx, servicedata.CreateURN("system", "foo")).Return(servicedata.Key{ + ResourceID: "724bed16-b971-499d-99d7-cf742484eafe", + }, nil) + rs.EXPECT().CheckPermission(ctx, user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, "724bed16-b971-499d-99d7-cf742484eafe", action.Action{ID: schema.EditPermission}).Return( + true, nil) gs.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), group.Group{ Name: "some group", Slug: "some-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{ ID: someGroupID, Name: "some group", @@ -373,12 +451,30 @@ func TestHandler_CreateGroup(t *testing.T) { OrganizationID: someOrgID, Metadata: metadata.Metadata{}, }, nil) - return user.SetContextWithEmail(ctx, email) + sds.EXPECT().Upsert(ctx, servicedata.ServiceData{ + EntityID: someGroupID, + NamespaceID: groupNamespaceID, + Key: servicedata.Key{ + Name: "foo", + ProjectID: "system", + }, + Value: "bar", + }).Return(servicedata.ServiceData{ + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, nil) + return ctx }, request: &shieldv1beta1.CreateGroupRequest{Body: &shieldv1beta1.GroupRequestBody{ - Name: "some group", - OrgId: someOrgID, - Metadata: &structpb.Struct{}, + Name: "some group", + OrgId: someOrgID, + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, }}, want: &shieldv1beta1.CreateGroupResponse{ Group: &shieldv1beta1.Group{ @@ -387,7 +483,9 @@ func TestHandler_CreateGroup(t *testing.T) { Slug: "some-group", OrgId: someOrgID, Metadata: &structpb.Struct{ - Fields: make(map[string]*structpb.Value), + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, }, CreatedAt: timestamppb.New(time.Time{}), UpdatedAt: timestamppb.New(time.Time{}), @@ -399,12 +497,21 @@ func TestHandler_CreateGroup(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockGroupSvc := new(mocks.GroupService) + mockUserSvc := new(mocks.UserService) + mockRelationSrv := new(mocks.RelationService) + mockServiceDataSvc := new(mocks.ServiceDataService) ctx := context.TODO() if tt.setup != nil { - ctx = tt.setup(ctx, mockGroupSvc) + ctx = tt.setup(ctx, mockGroupSvc, mockUserSvc, mockRelationSrv, mockServiceDataSvc) } h := Handler{ - groupService: mockGroupSvc, + serviceDataConfig: ServiceDataConfig{ + DefaultServiceDataProject: "system", + }, + groupService: mockGroupSvc, + userService: mockUserSvc, + serviceDataService: mockServiceDataSvc, + relationService: mockRelationSrv, } got, err := h.CreateGroup(ctx, tt.request) assert.EqualValues(t, got, tt.want) @@ -417,14 +524,15 @@ func TestHandler_GetGroup(t *testing.T) { someGroupID := uuid.NewString() tests := []struct { name string - setup func(gs *mocks.GroupService) + setup func(gs *mocks.GroupService, sds *mocks.ServiceDataService, us *mocks.UserService) request *shieldv1beta1.GetGroupRequest want *shieldv1beta1.GetGroupResponse wantErr error }{ { name: "should return internal error if group service return some error", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, sds *mocks.ServiceDataService, us *mocks.UserService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) gs.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), someGroupID).Return(group.Group{}, errors.New("some error")) }, request: &shieldv1beta1.GetGroupRequest{Id: someGroupID}, @@ -433,7 +541,8 @@ func TestHandler_GetGroup(t *testing.T) { }, { name: "should return not found error if id is invalid", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, sds *mocks.ServiceDataService, us *mocks.UserService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) gs.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "").Return(group.Group{}, group.ErrInvalidID) }, request: &shieldv1beta1.GetGroupRequest{}, @@ -442,7 +551,8 @@ func TestHandler_GetGroup(t *testing.T) { }, { name: "should return not found error if group not exist", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, sds *mocks.ServiceDataService, us *mocks.UserService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) gs.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "").Return(group.Group{}, group.ErrNotExist) }, request: &shieldv1beta1.GetGroupRequest{}, @@ -451,8 +561,22 @@ func TestHandler_GetGroup(t *testing.T) { }, { name: "should return success if group service return nil", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, sds *mocks.ServiceDataService, us *mocks.UserService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) gs.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), testGroupID).Return(testGroupMap[testGroupID], nil) + + sds.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), servicedata.Filter{ + ID: testGroupID, + Namespace: groupNamespaceID, + Entities: maps.Values(map[string]string{ + "group": groupNamespaceID, + }), + }).Return([]servicedata.ServiceData{{ + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }}, nil) }, request: &shieldv1beta1.GetGroupRequest{Id: testGroupID}, want: &shieldv1beta1.GetGroupResponse{ @@ -476,11 +600,15 @@ func TestHandler_GetGroup(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockGroupSvc := new(mocks.GroupService) + mockServiceDataSvc := new(mocks.ServiceDataService) + mockUserSvc := new(mocks.UserService) if tt.setup != nil { - tt.setup(mockGroupSvc) + tt.setup(mockGroupSvc, mockServiceDataSvc, mockUserSvc) } h := Handler{ - groupService: mockGroupSvc, + groupService: mockGroupSvc, + serviceDataService: mockServiceDataSvc, + userService: mockUserSvc, } got, err := h.GetGroup(context.TODO(), tt.request) assert.EqualValues(t, got, tt.want) @@ -494,21 +622,24 @@ func TestHandler_UpdateGroup(t *testing.T) { someOrgID := uuid.NewString() tests := []struct { name string - setup func(gs *mocks.GroupService) + setup func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) request *shieldv1beta1.UpdateGroupRequest want *shieldv1beta1.UpdateGroupResponse wantErr error }{ { name: "should return internal error if group service return some error", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Name: "new group", Slug: "new-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, errors.New("some error")) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -533,13 +664,16 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return not found error if group id is not uuid (slug) and does not exist", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ Name: "new group", Slug: "some-id", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrNotExist) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -555,14 +689,17 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return not found error if group id is uuid and does not exist", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Name: "new group", Slug: "new-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrNotExist) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -578,13 +715,16 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return not found error if group id is empty", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ Name: "new group", Slug: "", // consider it by slug and make the slug empty OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrInvalidID) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -599,14 +739,17 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return already exist error if group service return error conflict", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Name: "new group", Slug: "new-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrConflict) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -622,14 +765,17 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return bad request error if org id does not exist", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Name: "new group", Slug: "new-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, organization.ErrNotExist) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -645,14 +791,17 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return bad request error if org id is not uuid", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Name: "new group", Slug: "new-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, organization.ErrInvalidUUID) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -668,13 +817,16 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return bad request error if name is empty", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Slug: "new-group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrInvalidDetail) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -689,13 +841,16 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return bad request error if slug is empty", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Name: "new group", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{}, group.ErrInvalidDetail) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -710,21 +865,22 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return success if updated by id and group service return nil error", - setup: func(gs *mocks.GroupService) { + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user@gotocompany.com", + }, nil) gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ ID: someGroupID, Name: "new group", Slug: "new-group", OrganizationID: someOrgID, - - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{ ID: someGroupID, Name: "new group", Slug: "new-group", OrganizationID: someOrgID, - - Metadata: metadata.Metadata{}, + Metadata: metadata.Metadata{}, }, nil) }, request: &shieldv1beta1.UpdateGroupRequest{ @@ -752,13 +908,26 @@ func TestHandler_UpdateGroup(t *testing.T) { }, { name: "should return success if updated by slug and group service return nil error", - setup: func(gs *mocks.GroupService) { - gs.EXPECT().Update(mock.AnythingOfType("context.todoCtx"), group.Group{ + setup: func(gs *mocks.GroupService, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + ctx := mock.AnythingOfType("context.todoCtx") + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + Email: "user@gotocompany.com", + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) + sds.EXPECT().GetKeyByURN(ctx, servicedata.CreateURN("system", "foo")).Return(servicedata.Key{ + ResourceID: "724bed16-b971-499d-99d7-cf742484eafe", + }, nil) + rs.EXPECT().CheckPermission(ctx, user.User{ + Email: "user@gotocompany.com", + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, "724bed16-b971-499d-99d7-cf742484eafe", action.Action{ID: schema.EditPermission}).Return( + true, nil) + gs.EXPECT().Update(ctx, group.Group{ Name: "new group", Slug: "some-slug", OrganizationID: someOrgID, - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(group.Group{ ID: someGroupID, Name: "new group", @@ -767,6 +936,20 @@ func TestHandler_UpdateGroup(t *testing.T) { Metadata: metadata.Metadata{}, }, nil) + sds.EXPECT().Upsert(ctx, servicedata.ServiceData{ + EntityID: someGroupID, + NamespaceID: groupNamespaceID, + Key: servicedata.Key{ + Name: "foo", + ProjectID: "system", + }, + Value: "bar", + }).Return(servicedata.ServiceData{ + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, nil) }, request: &shieldv1beta1.UpdateGroupRequest{ Id: "some-slug", @@ -774,6 +957,11 @@ func TestHandler_UpdateGroup(t *testing.T) { Name: "new group", Slug: "new-group", // will be ignored OrgId: someOrgID, + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, }, }, want: &shieldv1beta1.UpdateGroupResponse{ @@ -783,7 +971,9 @@ func TestHandler_UpdateGroup(t *testing.T) { Slug: "some-slug", OrgId: someOrgID, Metadata: &structpb.Struct{ - Fields: make(map[string]*structpb.Value), + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, }, CreatedAt: timestamppb.New(time.Time{}), UpdatedAt: timestamppb.New(time.Time{}), @@ -795,11 +985,20 @@ func TestHandler_UpdateGroup(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockGroupSvc := new(mocks.GroupService) + mockUserSvc := new(mocks.UserService) + mockServiceDataSvc := new(mocks.ServiceDataService) + mockRelationSvc := new(mocks.RelationService) if tt.setup != nil { - tt.setup(mockGroupSvc) + tt.setup(mockGroupSvc, mockUserSvc, mockServiceDataSvc, mockRelationSvc) } h := Handler{ - groupService: mockGroupSvc, + groupService: mockGroupSvc, + userService: mockUserSvc, + serviceDataService: mockServiceDataSvc, + relationService: mockRelationSvc, + serviceDataConfig: ServiceDataConfig{ + DefaultServiceDataProject: "system", + }, } got, err := h.UpdateGroup(context.TODO(), tt.request) assert.EqualValues(t, got, tt.want) diff --git a/internal/api/v1beta1/mocks/relation_service.go b/internal/api/v1beta1/mocks/relation_service.go index 8db707254..ce3b6ce4c 100644 --- a/internal/api/v1beta1/mocks/relation_service.go +++ b/internal/api/v1beta1/mocks/relation_service.go @@ -1,12 +1,19 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package mocks import ( context "context" - relation "github.com/goto/shield/core/relation" + action "github.com/goto/shield/core/action" + mock "github.com/stretchr/testify/mock" + + namespace "github.com/goto/shield/core/namespace" + + relation "github.com/goto/shield/core/relation" + + user "github.com/goto/shield/core/user" ) // RelationService is an autogenerated mock type for the RelationService type @@ -22,14 +29,66 @@ func (_m *RelationService) EXPECT() *RelationService_Expecter { return &RelationService_Expecter{mock: &_m.Mock} } +// CheckPermission provides a mock function with given fields: ctx, usr, resourceNS, resourceIdxa, _a4 +func (_m *RelationService) CheckPermission(ctx context.Context, usr user.User, resourceNS namespace.Namespace, resourceIdxa string, _a4 action.Action) (bool, error) { + ret := _m.Called(ctx, usr, resourceNS, resourceIdxa, _a4) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, user.User, namespace.Namespace, string, action.Action) (bool, error)); ok { + return rf(ctx, usr, resourceNS, resourceIdxa, _a4) + } + if rf, ok := ret.Get(0).(func(context.Context, user.User, namespace.Namespace, string, action.Action) bool); ok { + r0 = rf(ctx, usr, resourceNS, resourceIdxa, _a4) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, user.User, namespace.Namespace, string, action.Action) error); ok { + r1 = rf(ctx, usr, resourceNS, resourceIdxa, _a4) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationService_CheckPermission_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckPermission' +type RelationService_CheckPermission_Call struct { + *mock.Call +} + +// CheckPermission is a helper method to define mock.On call +// - ctx context.Context +// - usr user.User +// - resourceNS namespace.Namespace +// - resourceIdxa string +// - _a4 action.Action +func (_e *RelationService_Expecter) CheckPermission(ctx interface{}, usr interface{}, resourceNS interface{}, resourceIdxa interface{}, _a4 interface{}) *RelationService_CheckPermission_Call { + return &RelationService_CheckPermission_Call{Call: _e.mock.On("CheckPermission", ctx, usr, resourceNS, resourceIdxa, _a4)} +} + +func (_c *RelationService_CheckPermission_Call) Run(run func(ctx context.Context, usr user.User, resourceNS namespace.Namespace, resourceIdxa string, _a4 action.Action)) *RelationService_CheckPermission_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(user.User), args[2].(namespace.Namespace), args[3].(string), args[4].(action.Action)) + }) + return _c +} + +func (_c *RelationService_CheckPermission_Call) Return(_a0 bool, _a1 error) *RelationService_CheckPermission_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationService_CheckPermission_Call) RunAndReturn(run func(context.Context, user.User, namespace.Namespace, string, action.Action) (bool, error)) *RelationService_CheckPermission_Call { + _c.Call.Return(run) + return _c +} + // Create provides a mock function with given fields: ctx, rel func (_m *RelationService) Create(ctx context.Context, rel relation.RelationV2) (relation.RelationV2, error) { ret := _m.Called(ctx, rel) - if len(ret) == 0 { - panic("no return value specified for Create") - } - var r0 relation.RelationV2 var r1 error if rf, ok := ret.Get(0).(func(context.Context, relation.RelationV2) (relation.RelationV2, error)); ok { @@ -83,10 +142,6 @@ func (_c *RelationService_Create_Call) RunAndReturn(run func(context.Context, re func (_m *RelationService) DeleteV2(ctx context.Context, rel relation.RelationV2) error { ret := _m.Called(ctx, rel) - if len(ret) == 0 { - panic("no return value specified for DeleteV2") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, relation.RelationV2) error); ok { r0 = rf(ctx, rel) @@ -130,10 +185,6 @@ func (_c *RelationService_DeleteV2_Call) RunAndReturn(run func(context.Context, func (_m *RelationService) Get(ctx context.Context, id string) (relation.RelationV2, error) { ret := _m.Called(ctx, id) - if len(ret) == 0 { - panic("no return value specified for Get") - } - var r0 relation.RelationV2 var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (relation.RelationV2, error)); ok { @@ -187,10 +238,6 @@ func (_c *RelationService_Get_Call) RunAndReturn(run func(context.Context, strin func (_m *RelationService) GetRelationByFields(ctx context.Context, rel relation.RelationV2) (relation.RelationV2, error) { ret := _m.Called(ctx, rel) - if len(ret) == 0 { - panic("no return value specified for GetRelationByFields") - } - var r0 relation.RelationV2 var r1 error if rf, ok := ret.Get(0).(func(context.Context, relation.RelationV2) (relation.RelationV2, error)); ok { @@ -244,10 +291,6 @@ func (_c *RelationService_GetRelationByFields_Call) RunAndReturn(run func(contex func (_m *RelationService) List(ctx context.Context) ([]relation.RelationV2, error) { ret := _m.Called(ctx) - if len(ret) == 0 { - panic("no return value specified for List") - } - var r0 []relation.RelationV2 var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]relation.RelationV2, error)); ok { @@ -298,6 +341,64 @@ func (_c *RelationService_List_Call) RunAndReturn(run func(context.Context) ([]r return _c } +// LookupResources provides a mock function with given fields: ctx, resourceType, permission, subjectType, subjectID +func (_m *RelationService) LookupResources(ctx context.Context, resourceType string, permission string, subjectType string, subjectID string) ([]string, error) { + ret := _m.Called(ctx, resourceType, permission, subjectType, subjectID) + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]string, error)); ok { + return rf(ctx, resourceType, permission, subjectType, subjectID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []string); ok { + r0 = rf(ctx, resourceType, permission, subjectType, subjectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { + r1 = rf(ctx, resourceType, permission, subjectType, subjectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationService_LookupResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LookupResources' +type RelationService_LookupResources_Call struct { + *mock.Call +} + +// LookupResources is a helper method to define mock.On call +// - ctx context.Context +// - resourceType string +// - permission string +// - subjectType string +// - subjectID string +func (_e *RelationService_Expecter) LookupResources(ctx interface{}, resourceType interface{}, permission interface{}, subjectType interface{}, subjectID interface{}) *RelationService_LookupResources_Call { + return &RelationService_LookupResources_Call{Call: _e.mock.On("LookupResources", ctx, resourceType, permission, subjectType, subjectID)} +} + +func (_c *RelationService_LookupResources_Call) Run(run func(ctx context.Context, resourceType string, permission string, subjectType string, subjectID string)) *RelationService_LookupResources_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *RelationService_LookupResources_Call) Return(_a0 []string, _a1 error) *RelationService_LookupResources_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationService_LookupResources_Call) RunAndReturn(run func(context.Context, string, string, string, string) ([]string, error)) *RelationService_LookupResources_Call { + _c.Call.Return(run) + return _c +} + // NewRelationService creates a new instance of RelationService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewRelationService(t interface { diff --git a/internal/api/v1beta1/mocks/servicedata_service.go b/internal/api/v1beta1/mocks/servicedata_service.go index 2c8e5622a..798ff9685 100644 --- a/internal/api/v1beta1/mocks/servicedata_service.go +++ b/internal/api/v1beta1/mocks/servicedata_service.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package mocks @@ -26,10 +26,6 @@ func (_m *ServiceDataService) EXPECT() *ServiceDataService_Expecter { func (_m *ServiceDataService) CreateKey(ctx context.Context, key servicedata.Key) (servicedata.Key, error) { ret := _m.Called(ctx, key) - if len(ret) == 0 { - panic("no return value specified for CreateKey") - } - var r0 servicedata.Key var r1 error if rf, ok := ret.Get(0).(func(context.Context, servicedata.Key) (servicedata.Key, error)); ok { @@ -83,10 +79,6 @@ func (_c *ServiceDataService_CreateKey_Call) RunAndReturn(run func(context.Conte func (_m *ServiceDataService) Get(ctx context.Context, filter servicedata.Filter) ([]servicedata.ServiceData, error) { ret := _m.Called(ctx, filter) - if len(ret) == 0 { - panic("no return value specified for Get") - } - var r0 []servicedata.ServiceData var r1 error if rf, ok := ret.Get(0).(func(context.Context, servicedata.Filter) ([]servicedata.ServiceData, error)); ok { @@ -138,14 +130,63 @@ func (_c *ServiceDataService_Get_Call) RunAndReturn(run func(context.Context, se return _c } +// GetKeyByURN provides a mock function with given fields: ctx, urn +func (_m *ServiceDataService) GetKeyByURN(ctx context.Context, urn string) (servicedata.Key, error) { + ret := _m.Called(ctx, urn) + + var r0 servicedata.Key + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (servicedata.Key, error)); ok { + return rf(ctx, urn) + } + if rf, ok := ret.Get(0).(func(context.Context, string) servicedata.Key); ok { + r0 = rf(ctx, urn) + } else { + r0 = ret.Get(0).(servicedata.Key) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, urn) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ServiceDataService_GetKeyByURN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetKeyByURN' +type ServiceDataService_GetKeyByURN_Call struct { + *mock.Call +} + +// GetKeyByURN is a helper method to define mock.On call +// - ctx context.Context +// - urn string +func (_e *ServiceDataService_Expecter) GetKeyByURN(ctx interface{}, urn interface{}) *ServiceDataService_GetKeyByURN_Call { + return &ServiceDataService_GetKeyByURN_Call{Call: _e.mock.On("GetKeyByURN", ctx, urn)} +} + +func (_c *ServiceDataService_GetKeyByURN_Call) Run(run func(ctx context.Context, urn string)) *ServiceDataService_GetKeyByURN_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *ServiceDataService_GetKeyByURN_Call) Return(_a0 servicedata.Key, _a1 error) *ServiceDataService_GetKeyByURN_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ServiceDataService_GetKeyByURN_Call) RunAndReturn(run func(context.Context, string) (servicedata.Key, error)) *ServiceDataService_GetKeyByURN_Call { + _c.Call.Return(run) + return _c +} + // Upsert provides a mock function with given fields: ctx, serviceData func (_m *ServiceDataService) Upsert(ctx context.Context, serviceData servicedata.ServiceData) (servicedata.ServiceData, error) { ret := _m.Called(ctx, serviceData) - if len(ret) == 0 { - panic("no return value specified for Upsert") - } - var r0 servicedata.ServiceData var r1 error if rf, ok := ret.Get(0).(func(context.Context, servicedata.ServiceData) (servicedata.ServiceData, error)); ok { diff --git a/internal/api/v1beta1/relation.go b/internal/api/v1beta1/relation.go index afbff76f6..5311d7186 100644 --- a/internal/api/v1beta1/relation.go +++ b/internal/api/v1beta1/relation.go @@ -27,6 +27,8 @@ type RelationService interface { List(ctx context.Context) ([]relation.RelationV2, error) DeleteV2(ctx context.Context, rel relation.RelationV2) error GetRelationByFields(ctx context.Context, rel relation.RelationV2) (relation.RelationV2, error) + LookupResources(ctx context.Context, resourceType, permission, subjectType, subjectID string) ([]string, error) + CheckPermission(ctx context.Context, usr user.User, resourceNS namespace.Namespace, resourceIdxa string, action action.Action) (bool, error) } var grpcRelationNotFoundErr = status.Errorf(codes.NotFound, "relation doesn't exist") diff --git a/internal/api/v1beta1/servicedata.go b/internal/api/v1beta1/servicedata.go index 3d3f977aa..0d176cb6e 100644 --- a/internal/api/v1beta1/servicedata.go +++ b/internal/api/v1beta1/servicedata.go @@ -13,6 +13,7 @@ import ( "github.com/goto/shield/core/servicedata" "github.com/goto/shield/core/user" "github.com/goto/shield/internal/schema" + errPkg "github.com/goto/shield/pkg/errors" shieldv1beta1 "github.com/goto/shield/proto/v1beta1" grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "golang.org/x/exp/maps" @@ -33,6 +34,7 @@ type ServiceDataService interface { CreateKey(ctx context.Context, key servicedata.Key) (servicedata.Key, error) Upsert(ctx context.Context, serviceData servicedata.ServiceData) (servicedata.ServiceData, error) Get(ctx context.Context, filter servicedata.Filter) ([]servicedata.ServiceData, error) + GetKeyByURN(ctx context.Context, urn string) (servicedata.Key, error) } func (h Handler) CreateServiceDataKey(ctx context.Context, request *shieldv1beta1.CreateServiceDataKeyRequest) (*shieldv1beta1.CreateServiceDataKeyResponse, error) { @@ -45,7 +47,7 @@ func (h Handler) CreateServiceDataKey(ctx context.Context, request *shieldv1beta keyResp, err := h.serviceDataService.CreateKey(ctx, servicedata.Key{ ProjectID: requestBody.GetProject(), - Key: requestBody.GetKey(), + Name: requestBody.GetKey(), Description: requestBody.GetDescription(), }) if err != nil { @@ -87,7 +89,13 @@ func (h Handler) UpsertUserServiceData(ctx context.Context, request *shieldv1bet return nil, grpcBadBodyError } - if len(requestBody.Data) > h.serviceDataConfig.MaxUpsert { + data := requestBody.GetData() + if data == nil { + return nil, grpcBadBodyError + } + sdMap := data.AsMap() + + if len(sdMap) > h.serviceDataConfig.MaxUpsert { return nil, grpcBadBodyError } @@ -104,13 +112,13 @@ func (h Handler) UpsertUserServiceData(ctx context.Context, request *shieldv1bet return nil, grpcInternalServerError } } - serviceDataMap := map[string]string{} - for k, v := range requestBody.Data { + serviceDataMap := map[string]any{} + for k, v := range sdMap { serviceDataResp, err := h.serviceDataService.Upsert(ctx, servicedata.ServiceData{ EntityID: userEntity.ID, NamespaceID: userNamespaceID, Key: servicedata.Key{ - Key: k, + Name: k, ProjectID: requestBody.Project, }, Value: v, @@ -121,6 +129,8 @@ func (h Handler) UpsertUserServiceData(ctx context.Context, request *shieldv1bet switch { case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): return nil, grpcUnauthenticated + case errors.Is(err, errPkg.ErrForbidden): + return nil, grpcPermissionDenied case errors.Is(err, project.ErrNotExist), errors.Is(err, servicedata.ErrInvalidDetail), errors.Is(err, relation.ErrInvalidDetail), errors.Is(err, servicedata.ErrNotExist): return nil, grpcBadBodyError @@ -128,11 +138,16 @@ func (h Handler) UpsertUserServiceData(ctx context.Context, request *shieldv1bet return nil, grpcInternalServerError } } - serviceDataMap[serviceDataResp.Key.Key] = serviceDataResp.Value + serviceDataMap[serviceDataResp.Key.Name] = serviceDataResp.Value + } + + serviceDataMapPB, err := structpb.NewStruct(serviceDataMap) + if err != nil { + return nil, grpcInternalServerError } return &shieldv1beta1.UpsertUserServiceDataResponse{ - Data: serviceDataMap, + Data: serviceDataMapPB, }, nil } @@ -148,7 +163,13 @@ func (h Handler) UpsertGroupServiceData(ctx context.Context, request *shieldv1be return nil, grpcBadBodyError } - if len(requestBody.Data) > h.serviceDataConfig.MaxUpsert { + data := requestBody.GetData() + if data == nil { + return nil, grpcBadBodyError + } + sdMap := data.AsMap() + + if len(sdMap) > h.serviceDataConfig.MaxUpsert { return nil, grpcBadBodyError } @@ -165,13 +186,13 @@ func (h Handler) UpsertGroupServiceData(ctx context.Context, request *shieldv1be return nil, grpcInternalServerError } } - serviceDataMap := map[string]string{} - for k, v := range requestBody.Data { + serviceDataMap := map[string]any{} + for k, v := range sdMap { serviceDataResp, err := h.serviceDataService.Upsert(ctx, servicedata.ServiceData{ EntityID: groupEntity.ID, NamespaceID: groupNamespaceID, Key: servicedata.Key{ - Key: k, + Name: k, ProjectID: requestBody.Project, }, Value: v, @@ -189,11 +210,16 @@ func (h Handler) UpsertGroupServiceData(ctx context.Context, request *shieldv1be return nil, grpcInternalServerError } } - serviceDataMap[serviceDataResp.Key.Key] = serviceDataResp.Value + serviceDataMap[serviceDataResp.Key.Name] = serviceDataResp.Value + } + + serviceDataMapPB, err := structpb.NewStruct(serviceDataMap) + if err != nil { + return nil, grpcInternalServerError } return &shieldv1beta1.UpsertGroupServiceDataResponse{ - Data: serviceDataMap, + Data: serviceDataMapPB, }, nil } @@ -313,7 +339,7 @@ func transformServiceDataKeyToPB(from servicedata.Key) (shieldv1beta1.ServiceDat } func transformServiceDataListToPB(from []servicedata.ServiceData) (*structpb.Struct, error) { - data := map[string]map[string]map[string]string{} + data := map[string]map[string]map[string]any{} for _, sd := range from { prjKey := fmt.Sprintf("%s:%s", projectNamespaceID, sd.Key.ProjectID) @@ -322,15 +348,15 @@ func transformServiceDataListToPB(from []servicedata.ServiceData) (*structpb.Str if ok { ent, ok := prj[entKey] if ok { - ent[sd.Key.Key] = sd.Value + ent[sd.Key.Name] = sd.Value } else { - prj[entKey] = map[string]string{ - sd.Key.Key: sd.Value, + prj[entKey] = map[string]any{ + sd.Key.Name: sd.Value, } } } else { - kv := map[string]string{sd.Key.Key: sd.Value} - data[prjKey] = map[string]map[string]string{ + kv := map[string]any{sd.Key.Name: sd.Value} + data[prjKey] = map[string]map[string]any{ entKey: kv, } } diff --git a/internal/api/v1beta1/servicedata_test.go b/internal/api/v1beta1/servicedata_test.go index 4d0d99195..a425331b9 100644 --- a/internal/api/v1beta1/servicedata_test.go +++ b/internal/api/v1beta1/servicedata_test.go @@ -14,6 +14,7 @@ import ( shieldv1beta1 "github.com/goto/shield/proto/v1beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "google.golang.org/protobuf/types/known/structpb" ) var ( @@ -27,7 +28,7 @@ var ( ID: testKeyID, URN: "key-urn", ProjectID: testKeyProjectID, - Key: testKeyName, + Name: testKeyName, Description: "test description", ResourceID: testKeyResourceID, } @@ -39,7 +40,7 @@ var ( EntityID: testEntityID, NamespaceID: userNamespaceID, Key: servicedata.Key{ - Key: testKeyName, + Name: testKeyName, ProjectID: testKeyProjectID, }, Value: testValue, @@ -48,7 +49,7 @@ var ( EntityID: testEntityID, NamespaceID: groupNamespaceID, Key: servicedata.Key{ - Key: testKeyName, + Name: testKeyName, ProjectID: testKeyProjectID, }, Value: testValue, @@ -87,7 +88,7 @@ func TestHandler_CreateKey(t *testing.T) { setup: func(ctx context.Context, ss *mocks.ServiceDataService) context.Context { ss.EXPECT().CreateKey(mock.AnythingOfType("*context.valueCtx"), servicedata.Key{ ProjectID: "non-existing-project", - Key: testKey.Key, + Name: testKey.Name, Description: testKey.Description, }).Return(servicedata.Key{}, project.ErrNotExist) return user.SetContextWithEmail(ctx, email) @@ -95,7 +96,7 @@ func TestHandler_CreateKey(t *testing.T) { request: &shieldv1beta1.CreateServiceDataKeyRequest{ Body: &shieldv1beta1.ServiceDataKeyRequestBody{ Project: "non-existing-project", - Key: testKey.Key, + Key: testKey.Name, Description: testKey.Description, }, }, @@ -107,7 +108,7 @@ func TestHandler_CreateKey(t *testing.T) { setup: func(ctx context.Context, ss *mocks.ServiceDataService) context.Context { ss.EXPECT().CreateKey(mock.AnythingOfType("*context.valueCtx"), servicedata.Key{ ProjectID: testKey.ProjectID, - Key: testKey.Key, + Name: testKey.Name, Description: testKey.Description, }).Return(servicedata.Key{}, servicedata.ErrConflict) return user.SetContextWithEmail(ctx, email) @@ -115,7 +116,7 @@ func TestHandler_CreateKey(t *testing.T) { request: &shieldv1beta1.CreateServiceDataKeyRequest{ Body: &shieldv1beta1.ServiceDataKeyRequestBody{ Project: testKey.ProjectID, - Key: testKey.Key, + Key: testKey.Name, Description: testKey.Description, }, }, @@ -127,7 +128,7 @@ func TestHandler_CreateKey(t *testing.T) { setup: func(ctx context.Context, ss *mocks.ServiceDataService) context.Context { ss.EXPECT().CreateKey(mock.AnythingOfType("*context.valueCtx"), servicedata.Key{ ProjectID: testKey.ProjectID, - Key: testKey.Key, + Name: testKey.Name, Description: testKey.Description, }).Return(servicedata.Key{}, relation.ErrInvalidDetail) return user.SetContextWithEmail(ctx, email) @@ -135,7 +136,7 @@ func TestHandler_CreateKey(t *testing.T) { request: &shieldv1beta1.CreateServiceDataKeyRequest{ Body: &shieldv1beta1.ServiceDataKeyRequestBody{ Project: testKey.ProjectID, - Key: testKey.Key, + Key: testKey.Name, Description: testKey.Description, }, }, @@ -147,7 +148,7 @@ func TestHandler_CreateKey(t *testing.T) { setup: func(ctx context.Context, ss *mocks.ServiceDataService) context.Context { ss.EXPECT().CreateKey(mock.AnythingOfType("*context.valueCtx"), servicedata.Key{ ProjectID: testKey.ProjectID, - Key: testKey.Key, + Name: testKey.Name, Description: testKey.Description, }).Return(servicedata.Key{}, servicedata.ErrInvalidDetail) return user.SetContextWithEmail(ctx, email) @@ -155,7 +156,7 @@ func TestHandler_CreateKey(t *testing.T) { request: &shieldv1beta1.CreateServiceDataKeyRequest{ Body: &shieldv1beta1.ServiceDataKeyRequestBody{ Project: testKey.ProjectID, - Key: testKey.Key, + Key: testKey.Name, Description: testKey.Description, }, }, @@ -167,7 +168,7 @@ func TestHandler_CreateKey(t *testing.T) { setup: func(ctx context.Context, ss *mocks.ServiceDataService) context.Context { ss.EXPECT().CreateKey(mock.AnythingOfType("*context.valueCtx"), servicedata.Key{ ProjectID: testKey.ProjectID, - Key: testKey.Key, + Name: testKey.Name, Description: testKey.Description, }).Return(servicedata.Key{ ID: testKey.ID, @@ -178,7 +179,7 @@ func TestHandler_CreateKey(t *testing.T) { request: &shieldv1beta1.CreateServiceDataKeyRequest{ Body: &shieldv1beta1.ServiceDataKeyRequestBody{ Project: testKey.ProjectID, - Key: testKey.Key, + Key: testKey.Name, Description: testKey.Description, }, }, @@ -232,7 +233,7 @@ func TestHandler_UpdateUserServiceData(t *testing.T) { request: &shieldv1beta1.UpsertUserServiceDataRequest{ UserId: "", Body: &shieldv1beta1.UpsertServiceDataRequestBody{ - Data: map[string]string{}, + Data: &structpb.Struct{}, }, }, want: nil, @@ -243,9 +244,11 @@ func TestHandler_UpdateUserServiceData(t *testing.T) { request: &shieldv1beta1.UpsertUserServiceDataRequest{ UserId: "", Body: &shieldv1beta1.UpsertServiceDataRequestBody{ - Data: map[string]string{ - "test-key-1": "test-value-1", - "test-key-2": "test-value-2", + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "test-key-1": structpb.NewStringValue("test-value-1"), + "test-key-2": structpb.NewStringValue("test-value-2"), + }, }, }, }, @@ -257,8 +260,10 @@ func TestHandler_UpdateUserServiceData(t *testing.T) { request: &shieldv1beta1.UpsertUserServiceDataRequest{ UserId: "", Body: &shieldv1beta1.UpsertServiceDataRequestBody{ - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, @@ -275,8 +280,10 @@ func TestHandler_UpdateUserServiceData(t *testing.T) { UserId: testEntityID, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: testKeyProjectID, - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, @@ -294,8 +301,10 @@ func TestHandler_UpdateUserServiceData(t *testing.T) { UserId: testEntityID, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: testKeyProjectID, - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, @@ -313,22 +322,26 @@ func TestHandler_UpdateUserServiceData(t *testing.T) { UserId: testEntityID, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: testKeyProjectID, - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, setup: func(ctx context.Context, ss *mocks.ServiceDataService, us *mocks.UserService) context.Context { us.EXPECT().Get(mock.AnythingOfType("*context.valueCtx"), testEntityID).Return(user.User{ID: testEntityID}, nil) ss.EXPECT().Upsert(mock.AnythingOfType("*context.valueCtx"), testUserServiceDataCreate).Return(servicedata.ServiceData{ - Key: servicedata.Key{Key: testKeyName}, + Key: servicedata.Key{Name: testKeyName}, Value: testValue, }, nil) return user.SetContextWithEmail(ctx, email) }, want: &shieldv1beta1.UpsertUserServiceDataResponse{ - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, wantErr: nil, @@ -379,7 +392,7 @@ func TestHandler_UpdateGroupServiceData(t *testing.T) { request: &shieldv1beta1.UpsertGroupServiceDataRequest{ GroupId: "", Body: &shieldv1beta1.UpsertServiceDataRequestBody{ - Data: map[string]string{}, + Data: &structpb.Struct{}, }, }, want: nil, @@ -390,9 +403,11 @@ func TestHandler_UpdateGroupServiceData(t *testing.T) { request: &shieldv1beta1.UpsertGroupServiceDataRequest{ GroupId: "", Body: &shieldv1beta1.UpsertServiceDataRequestBody{ - Data: map[string]string{ - "test-key-1": "test-value-1", - "test-key-2": "test-value-2", + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "test-key-1": structpb.NewStringValue("test-value-1"), + "test-key-2": structpb.NewStringValue("test-value-2"), + }, }, }, }, @@ -404,8 +419,10 @@ func TestHandler_UpdateGroupServiceData(t *testing.T) { request: &shieldv1beta1.UpsertGroupServiceDataRequest{ GroupId: testEntityID, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, @@ -422,8 +439,10 @@ func TestHandler_UpdateGroupServiceData(t *testing.T) { GroupId: testEntityID, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: testKeyProjectID, - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, @@ -441,8 +460,10 @@ func TestHandler_UpdateGroupServiceData(t *testing.T) { GroupId: testEntityID, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: testKeyProjectID, - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, @@ -460,22 +481,26 @@ func TestHandler_UpdateGroupServiceData(t *testing.T) { GroupId: testEntityID, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: testKeyProjectID, - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, }, setup: func(ctx context.Context, ss *mocks.ServiceDataService, gs *mocks.GroupService) context.Context { gs.EXPECT().Get(mock.AnythingOfType("*context.valueCtx"), testEntityID).Return(group.Group{ID: testEntityID}, nil) ss.EXPECT().Upsert(mock.AnythingOfType("*context.valueCtx"), testGroupServiceDataCreate).Return(servicedata.ServiceData{ - Key: servicedata.Key{Key: testKeyName}, + Key: servicedata.Key{Name: testKeyName}, Value: testValue, }, nil) return user.SetContextWithEmail(ctx, email) }, want: &shieldv1beta1.UpsertGroupServiceDataResponse{ - Data: map[string]string{ - testKeyName: testValue, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + testKeyName: structpb.NewStringValue(testValue), + }, }, }, wantErr: nil, diff --git a/internal/api/v1beta1/user.go b/internal/api/v1beta1/user.go index b8ee818b9..2a12cc832 100644 --- a/internal/api/v1beta1/user.go +++ b/internal/api/v1beta1/user.go @@ -3,18 +3,27 @@ package v1beta1 import ( "context" "errors" + "fmt" "net/mail" "strings" grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "golang.org/x/exp/maps" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/goto/shield/core/action" + "github.com/goto/shield/core/namespace" + "github.com/goto/shield/core/project" + "github.com/goto/shield/core/relation" + "github.com/goto/shield/core/servicedata" "github.com/goto/shield/core/user" + "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/metadata" + errorsPkg "github.com/goto/shield/pkg/errors" "github.com/goto/shield/pkg/uuid" shieldv1beta1 "github.com/goto/shield/proto/v1beta1" ) @@ -37,10 +46,30 @@ func (h Handler) ListUsers(ctx context.Context, request *shieldv1beta1.ListUsers logger := grpczap.Extract(ctx) var users []*shieldv1beta1.User + currentUser, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) + return nil, grpcUnauthenticated + } + + servicedataKeyResourceIds, err := h.relationService.LookupResources(ctx, schema.ServiceDataKeyNamespace, schema.ViewPermission, schema.UserPrincipal, currentUser.ID) + if err != nil { + logger.Error(err.Error()) + return nil, grpcInternalServerError + } + + prj, err := h.projectService.Get(ctx, h.serviceDataConfig.DefaultServiceDataProject) + if err != nil { + logger.Error(err.Error()) + return nil, grpcInternalServerError + } + userResp, err := h.userService.List(ctx, user.Filter{ - Limit: request.GetPageSize(), - Page: request.GetPageNum(), - Keyword: request.GetKeyword(), + Limit: request.GetPageSize(), + Page: request.GetPageNum(), + Keyword: request.GetKeyword(), + ProjectID: prj.ID, + ServiceDataKeyResourceIds: servicedataKeyResourceIds, }) if err != nil { logger.Error(err.Error()) @@ -67,14 +96,9 @@ func (h Handler) ListUsers(ctx context.Context, request *shieldv1beta1.ListUsers func (h Handler) CreateUser(ctx context.Context, request *shieldv1beta1.CreateUserRequest) (*shieldv1beta1.CreateUserResponse, error) { logger := grpczap.Extract(ctx) - currentUserEmail, ok := user.GetEmailFromContext(ctx) - if !ok { - return nil, grpcUnauthenticated - } - - currentUserEmail = strings.TrimSpace(currentUserEmail) - if currentUserEmail == "" { - logger.Error(ErrEmptyEmailID.Error()) + currentUser, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) return nil, grpcUnauthenticated } @@ -84,9 +108,8 @@ func (h Handler) CreateUser(ctx context.Context, request *shieldv1beta1.CreateUs email := strings.TrimSpace(request.GetBody().GetEmail()) if email == "" { - email = currentUserEmail + return nil, grpcBadBodyError } - if !isValidEmail(email) { return nil, user.ErrInvalidEmail } @@ -97,11 +120,27 @@ func (h Handler) CreateUser(ctx context.Context, request *shieldv1beta1.CreateUs return nil, grpcBadBodyError } + for k := range metaDataMap { + urn := servicedata.CreateURN(h.serviceDataConfig.DefaultServiceDataProject, k) + key, err := h.serviceDataService.GetKeyByURN(ctx, urn) + if err != nil { + return nil, err + } + + permission, err := h.relationService.CheckPermission(ctx, currentUser, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, key.ResourceID, action.Action{ID: schema.EditPermission}) + if err != nil { + return nil, err + } + if !permission { + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + } + } + // TODO might need to check the valid email form newUser, err := h.userService.Create(ctx, user.User{ Name: request.GetBody().GetName(), Email: email, - Metadata: metaDataMap, + Metadata: nil, }) if err != nil { logger.Error(err.Error()) @@ -118,20 +157,45 @@ func (h Handler) CreateUser(ctx context.Context, request *shieldv1beta1.CreateUs } } - metaData, err := newUser.Metadata.ToStructPB() + serviceDataMap := map[string]any{} + for k, v := range metaDataMap { + serviceDataResp, err := h.serviceDataService.Upsert(ctx, servicedata.ServiceData{ + EntityID: newUser.ID, + NamespaceID: userNamespaceID, + Key: servicedata.Key{ + Name: k, + ProjectID: h.serviceDataConfig.DefaultServiceDataProject, + }, + Value: v, + }) + if err != nil { + logger.Error(err.Error()) + + switch { + case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): + return nil, grpcUnauthenticated + case errors.Is(err, project.ErrNotExist), errors.Is(err, servicedata.ErrInvalidDetail), + errors.Is(err, relation.ErrInvalidDetail), errors.Is(err, servicedata.ErrNotExist): + return nil, grpcBadBodyError + case errors.Is(err, errorsPkg.ErrForbidden): + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + default: + return nil, grpcInternalServerError + } + } + serviceDataMap[serviceDataResp.Key.Name] = serviceDataResp.Value + } + + // TODO: use serviceDataMap + newUser.Metadata = metaDataMap + + userPB, err := transformUserToPB(newUser) if err != nil { logger.Error(err.Error()) - return nil, grpcInternalServerError + return nil, ErrInternalServer } - return &shieldv1beta1.CreateUserResponse{User: &shieldv1beta1.User{ - Id: newUser.ID, - Name: newUser.Name, - Email: newUser.Email, - Metadata: metaData, - CreatedAt: timestamppb.New(newUser.CreatedAt), - UpdatedAt: timestamppb.New(newUser.UpdatedAt), - }}, nil + return &shieldv1beta1.CreateUserResponse{User: &userPB}, nil } func (h Handler) CreateMetadataKey(ctx context.Context, request *shieldv1beta1.CreateMetadataKeyRequest) (*shieldv1beta1.CreateMetadataKeyResponse, error) { @@ -167,6 +231,12 @@ func (h Handler) CreateMetadataKey(ctx context.Context, request *shieldv1beta1.C func (h Handler) GetUser(ctx context.Context, request *shieldv1beta1.GetUserRequest) (*shieldv1beta1.GetUserResponse, error) { logger := grpczap.Extract(ctx) + _, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) + return nil, grpcUnauthenticated + } + fetchedUser, err := h.userService.Get(ctx, request.GetId()) if err != nil { logger.Error(err.Error()) @@ -178,6 +248,31 @@ func (h Handler) GetUser(ctx context.Context, request *shieldv1beta1.GetUserRequ } } + filter := servicedata.Filter{ + ID: fetchedUser.ID, + Namespace: userNamespaceID, + Entities: maps.Values(map[string]string{ + "user": userNamespaceID, + }), + } + + userSD, err := h.serviceDataService.Get(ctx, filter) + if err != nil { + logger.Error(err.Error()) + switch { + case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): + break + default: + return nil, grpcInternalServerError + } + } else { + metadata := map[string]any{} + for _, sd := range userSD { + metadata[sd.Key.Name] = sd.Value + } + fetchedUser.Metadata = metadata + } + userPB, err := transformUserToPB(fetchedUser) if err != nil { logger.Error(err.Error()) @@ -214,6 +309,31 @@ func (h Handler) GetCurrentUser(ctx context.Context, request *shieldv1beta1.GetC } } + filter := servicedata.Filter{ + ID: fetchedUser.ID, + Namespace: userNamespaceID, + Entities: maps.Values(map[string]string{ + "user": userNamespaceID, + }), + } + + userSD, err := h.serviceDataService.Get(ctx, filter) + if err != nil { + logger.Error(err.Error()) + switch { + case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): + break + default: + return nil, grpcInternalServerError + } + } else { + metadata := map[string]any{} + for _, sd := range userSD { + metadata[sd.Key.Name] = sd.Value + } + fetchedUser.Metadata = metadata + } + userPB, err := transformUserToPB(fetchedUser) if err != nil { logger.Error(err.Error()) @@ -229,6 +349,12 @@ func (h Handler) UpdateUser(ctx context.Context, request *shieldv1beta1.UpdateUs logger := grpczap.Extract(ctx) var updatedUser user.User + currentUser, err := h.userService.FetchCurrentUser(ctx) + if err != nil { + logger.Error(err.Error()) + return nil, grpcUnauthenticated + } + if strings.TrimSpace(request.GetId()) == "" { return nil, grpcUserNotFoundError } @@ -251,13 +377,29 @@ func (h Handler) UpdateUser(ctx context.Context, request *shieldv1beta1.UpdateUs return nil, grpcBadBodyError } + for k := range metaDataMap { + urn := servicedata.CreateURN(h.serviceDataConfig.DefaultServiceDataProject, k) + key, err := h.serviceDataService.GetKeyByURN(ctx, urn) + if err != nil { + return nil, err + } + + permission, err := h.relationService.CheckPermission(ctx, currentUser, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, key.ResourceID, action.Action{ID: schema.EditPermission}) + if err != nil { + return nil, err + } + if !permission { + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + } + } + id := request.GetId() if uuid.IsValid(id) { updatedUser, err = h.userService.UpdateByID(ctx, user.User{ ID: id, Name: request.GetBody().GetName(), Email: email, - Metadata: metaDataMap, + Metadata: nil, }) if err != nil { logger.Error(err.Error()) @@ -292,14 +434,58 @@ func (h Handler) UpdateUser(ctx context.Context, request *shieldv1beta1.UpdateUs updatedUser, err = h.userService.UpdateByEmail(ctx, user.User{ Name: request.GetBody().GetName(), Email: email, - Metadata: metaDataMap, + Metadata: nil, }) if err != nil { logger.Error(err.Error()) - return nil, grpcInternalServerError + switch { + case errors.Is(err, user.ErrNotExist), errors.Is(err, user.ErrInvalidID), errors.Is(err, user.ErrInvalidUUID): + return nil, grpcUserNotFoundError + case errors.Is(err, user.ErrInvalidEmail): + return nil, grpcBadBodyError + case errors.Is(err, user.ErrConflict): + return nil, grpcConflictError + case errors.Is(err, user.ErrInvalidEmail), + errors.Is(err, user.ErrMissingEmail): + return nil, grpcUnauthenticated + default: + return nil, grpcInternalServerError + } } } + serviceDataMap := map[string]any{} + for k, v := range metaDataMap { + serviceDataResp, err := h.serviceDataService.Upsert(ctx, servicedata.ServiceData{ + EntityID: updatedUser.ID, + NamespaceID: userNamespaceID, + Key: servicedata.Key{ + Name: k, + ProjectID: h.serviceDataConfig.DefaultServiceDataProject, + }, + Value: v, + }) + if err != nil { + logger.Error(err.Error()) + + switch { + case errors.Is(err, user.ErrInvalidEmail), errors.Is(err, user.ErrMissingEmail): + return nil, grpcUnauthenticated + case errors.Is(err, project.ErrNotExist), errors.Is(err, servicedata.ErrInvalidDetail), + errors.Is(err, relation.ErrInvalidDetail), errors.Is(err, servicedata.ErrNotExist): + return nil, grpcBadBodyError + case errors.Is(err, errorsPkg.ErrForbidden): + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("you are not authorized to update %s key", k)) + default: + return nil, grpcInternalServerError + } + } + serviceDataMap[serviceDataResp.Key.Name] = serviceDataResp.Value + } + + // Note: this would return only the keys that are updated in the current request + updatedUser.Metadata = metaDataMap + userPB, err := transformUserToPB(updatedUser) if err != nil { logger.Error(err.Error()) diff --git a/internal/api/v1beta1/user_test.go b/internal/api/v1beta1/user_test.go index 35afc3186..03f70819a 100644 --- a/internal/api/v1beta1/user_test.go +++ b/internal/api/v1beta1/user_test.go @@ -6,11 +6,18 @@ import ( "testing" "time" + "github.com/goto/shield/core/action" "github.com/goto/shield/core/group" + "github.com/goto/shield/core/namespace" + "github.com/goto/shield/core/project" + "github.com/goto/shield/core/servicedata" "github.com/goto/shield/core/user" "github.com/goto/shield/internal/api/v1beta1/mocks" + "github.com/goto/shield/internal/schema" + errorsPkg "github.com/goto/shield/pkg/errors" "github.com/goto/shield/pkg/metadata" "github.com/goto/shield/pkg/uuid" + "golang.org/x/exp/maps" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -44,14 +51,22 @@ var ( func TestListUsers(t *testing.T) { table := []struct { title string - setup func(us *mocks.UserService) + setup func(us *mocks.UserService, rs *mocks.RelationService, ps *mocks.ProjectService) req *shieldv1beta1.ListUsersRequest want *shieldv1beta1.ListUsersResponse err error }{ { title: "should return internal error in if user service return some error", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, rs *mocks.RelationService, ps *mocks.ProjectService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) + + rs.EXPECT().LookupResources(mock.AnythingOfType("context.todoCtx"), schema.ServiceDataKeyNamespace, schema.ViewPermission, schema.UserPrincipal, "083a77a2-ab14-40d2-a06d-f6d9f80c6378").Return([]string{}, nil) + ps.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "").Return(project.Project{ + ID: "11a58737-f366-4d05-b925-6f7bded29257", + }, nil) us.EXPECT().List(mock.Anything, mock.Anything).Return(user.PagedUsers{}, errors.New("some error")) }, req: &shieldv1beta1.ListUsersRequest{ @@ -63,11 +78,19 @@ func TestListUsers(t *testing.T) { err: status.Errorf(codes.Internal, ErrInternalServer.Error()), }, { title: "should return all users if user service return all users", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, rs *mocks.RelationService, ps *mocks.ProjectService) { var testUserList []user.User for _, u := range testUserMap { testUserList = append(testUserList, u) } + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + ID: "083a77a2-ab14-40d2-a06d-f6d9f80c6378", + }, nil) + + rs.EXPECT().LookupResources(mock.AnythingOfType("context.todoCtx"), schema.ServiceDataKeyNamespace, schema.ViewPermission, schema.UserPrincipal, "083a77a2-ab14-40d2-a06d-f6d9f80c6378").Return([]string{}, nil) + ps.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "").Return(project.Project{ + ID: "11a58737-f366-4d05-b925-6f7bded29257", + }, nil) us.EXPECT().List(mock.Anything, mock.Anything).Return( user.PagedUsers{ Users: testUserList, @@ -105,10 +128,16 @@ func TestListUsers(t *testing.T) { for _, tt := range table { t.Run(tt.title, func(t *testing.T) { mockUserSrv := new(mocks.UserService) + mockRelationSrv := new(mocks.RelationService) + mockProjectSvc := new(mocks.ProjectService) if tt.setup != nil { - tt.setup(mockUserSrv) + tt.setup(mockUserSrv, mockRelationSrv, mockProjectSvc) + } + mockDep := Handler{ + userService: mockUserSrv, + relationService: mockRelationSrv, + projectService: mockProjectSvc, } - mockDep := Handler{userService: mockUserSrv} req := tt.req resp, err := mockDep.ListUsers(context.TODO(), req) assert.EqualValues(t, resp, tt.want) @@ -121,13 +150,17 @@ func TestCreateUser(t *testing.T) { email := "user@gotocompany.com" table := []struct { title string - setup func(ctx context.Context, us *mocks.UserService) context.Context + setup func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context req *shieldv1beta1.CreateUserRequest want *shieldv1beta1.CreateUserResponse err error }{ { title: "should return unauthenticated error if no auth email header in context", + setup: func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context { + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{}, user.ErrMissingEmail) + return ctx + }, req: &shieldv1beta1.CreateUserRequest{Body: &shieldv1beta1.UserRequestBody{ Name: "some user", Email: "abc@test.com", @@ -138,8 +171,12 @@ func TestCreateUser(t *testing.T) { }, { title: "should return bad request error if metadata is not parsable", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { - return user.SetContextWithEmail(ctx, email) + setup: func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + Email: email, + }, nil) + return ctx }, req: &shieldv1beta1.CreateUserRequest{Body: &shieldv1beta1.UserRequestBody{ Name: "some user", @@ -155,11 +192,15 @@ func TestCreateUser(t *testing.T) { }, { title: "should return bad request error if email is empty", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { + setup: func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + Email: email, + }, nil) us.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), user.User{ Name: "some user", }).Return(user.User{}, user.ErrInvalidEmail) - return user.SetContextWithEmail(ctx, email) + return ctx }, req: &shieldv1beta1.CreateUserRequest{Body: &shieldv1beta1.UserRequestBody{ Name: "some user", @@ -175,8 +216,15 @@ func TestCreateUser(t *testing.T) { }, { title: "should return invalid email error if email is invalid", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { - return user.SetContextWithEmail(ctx, email) + setup: func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + Email: email, + }, nil) + us.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), user.User{ + Name: "some user", + }).Return(user.User{}, user.ErrInvalidEmail) + return ctx }, req: &shieldv1beta1.CreateUserRequest{Body: &shieldv1beta1.UserRequestBody{ Name: "some user", @@ -192,13 +240,17 @@ func TestCreateUser(t *testing.T) { }, { title: "should return already exist error if user service return error conflict", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { - us.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), user.User{ + setup: func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + Email: email, + }, nil) + us.EXPECT().Create(ctx, user.User{ Name: "some user", Email: "abc@test.com", - Metadata: metadata.Metadata{}, + Metadata: nil, }).Return(user.User{}, user.ErrConflict) - return user.SetContextWithEmail(ctx, email) + return ctx }, req: &shieldv1beta1.CreateUserRequest{Body: &shieldv1beta1.UserRequestBody{ Name: "some user", @@ -210,19 +262,41 @@ func TestCreateUser(t *testing.T) { }, { title: "should return success if user email contain whitespace but still valid service return nil error", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { + setup: func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + Email: email, + }, nil) us.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), user.User{ Name: "some user", Email: "abc@test.com", - Metadata: metadata.Metadata{"foo": "bar"}, + Metadata: nil, }).Return( user.User{ ID: "new-abc", Name: "some user", Email: "abc@test.com", - Metadata: metadata.Metadata{"foo": "bar"}, + Metadata: nil, }, nil) - return user.SetContextWithEmail(ctx, email) + sds.EXPECT().GetKeyByURN(ctx, ":servicedata_key:foo").Return(servicedata.Key{ + ResourceID: "8b88ed58-9fca-48fb-8dd4-aada1a06ba33", + }, nil) + rs.EXPECT().CheckPermission(ctx, user.User{Email: email}, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, "8b88ed58-9fca-48fb-8dd4-aada1a06ba33", action.Action{ID: schema.EditPermission}).Return(true, nil) + sds.EXPECT().Upsert(ctx, servicedata.ServiceData{ + EntityID: "new-abc", + NamespaceID: schema.UserPrincipal, + Key: servicedata.Key{ + Name: "foo", + ProjectID: "", + }, + Value: "bar", + }).Return(servicedata.ServiceData{ + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, nil) + return ctx }, req: &shieldv1beta1.CreateUserRequest{Body: &shieldv1beta1.UserRequestBody{ Name: "some user", @@ -249,19 +323,42 @@ func TestCreateUser(t *testing.T) { }, { title: "should return success if user service return nil error", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { + setup: func(ctx context.Context, us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().FetchCurrentUser(ctx).Return(user.User{ + Email: email, + }, nil) + sds.EXPECT().GetKeyByURN(ctx, ":servicedata_key:foo").Return(servicedata.Key{ + ResourceID: "8b88ed58-9fca-48fb-8dd4-aada1a06ba33", + }, nil) + rs.EXPECT().CheckPermission(ctx, user.User{Email: email}, namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, "8b88ed58-9fca-48fb-8dd4-aada1a06ba33", action.Action{ID: schema.EditPermission}).Return(true, nil) us.EXPECT().Create(mock.AnythingOfType("*context.valueCtx"), user.User{ Name: "some user", Email: "abc@test.com", - Metadata: metadata.Metadata{"foo": "bar"}, + Metadata: nil, }).Return( user.User{ ID: "new-abc", Name: "some user", Email: "abc@test.com", - Metadata: metadata.Metadata{"foo": "bar"}, + Metadata: nil, }, nil) - return user.SetContextWithEmail(ctx, email) + + sds.EXPECT().Upsert(ctx, servicedata.ServiceData{ + EntityID: "new-abc", + NamespaceID: schema.UserPrincipal, + Key: servicedata.Key{ + Name: "foo", + ProjectID: "", + }, + Value: "bar", + }).Return(servicedata.ServiceData{ + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, nil) + return ctx }, req: &shieldv1beta1.CreateUserRequest{Body: &shieldv1beta1.UserRequestBody{ Name: "some user", @@ -295,10 +392,16 @@ func TestCreateUser(t *testing.T) { ctx := context.TODO() mockUserSrv := new(mocks.UserService) + mockServiceDataSvc := new(mocks.ServiceDataService) + mockRelationSvc := new(mocks.RelationService) if tt.setup != nil { - ctx = tt.setup(ctx, mockUserSrv) + ctx = tt.setup(ctx, mockUserSrv, mockServiceDataSvc, mockRelationSvc) + } + mockDep := Handler{ + userService: mockUserSrv, + serviceDataService: mockServiceDataSvc, + relationService: mockRelationSvc, } - mockDep := Handler{userService: mockUserSrv} resp, err = mockDep.CreateUser(ctx, tt.req) assert.EqualValues(t, tt.want, resp) assert.EqualValues(t, tt.err, err) @@ -311,13 +414,14 @@ func TestGetUser(t *testing.T) { table := []struct { title string req *shieldv1beta1.GetUserRequest - setup func(us *mocks.UserService) + setup func(us *mocks.UserService, sd *mocks.ServiceDataService) want *shieldv1beta1.GetUserResponse err error }{ { title: "should return not found error if user does not exist", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sd *mocks.ServiceDataService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) us.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), randomID).Return(user.User{}, user.ErrNotExist) }, req: &shieldv1beta1.GetUserRequest{ @@ -328,7 +432,8 @@ func TestGetUser(t *testing.T) { }, { title: "should return not found error if user id is not uuid", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sd *mocks.ServiceDataService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) us.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "some-id").Return(user.User{}, user.ErrInvalidUUID) }, req: &shieldv1beta1.GetUserRequest{ @@ -339,7 +444,8 @@ func TestGetUser(t *testing.T) { }, { title: "should return not found error if user id is invalid", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sd *mocks.ServiceDataService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) us.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), "").Return(user.User{}, user.ErrInvalidID) }, req: &shieldv1beta1.GetUserRequest{}, @@ -348,18 +454,31 @@ func TestGetUser(t *testing.T) { }, { title: "should return user if user service return nil error", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sd *mocks.ServiceDataService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) us.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), randomID).Return( user.User{ - ID: randomID, - Name: "some user", - Email: "someuser@test.com", - Metadata: metadata.Metadata{ - "foo": "bar", - }, + ID: randomID, + Name: "some user", + Email: "someuser@test.com", CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, nil) + + sd.EXPECT().Get(mock.AnythingOfType("context.todoCtx"), servicedata.Filter{ + ID: randomID, + Namespace: userNamespaceID, + Entities: maps.Values(map[string]string{ + "user": userNamespaceID, + }), + }).Return([]servicedata.ServiceData{ + { + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, + }, nil) }, req: &shieldv1beta1.GetUserRequest{ Id: randomID, @@ -383,10 +502,12 @@ func TestGetUser(t *testing.T) { for _, tt := range table { t.Run(tt.title, func(t *testing.T) { mockUserSrv := new(mocks.UserService) + mockServiceDataSrv := new(mocks.ServiceDataService) + if tt.setup != nil { - tt.setup(mockUserSrv) + tt.setup(mockUserSrv, mockServiceDataSrv) } - mockDep := Handler{userService: mockUserSrv} + mockDep := Handler{userService: mockUserSrv, serviceDataService: mockServiceDataSrv} resp, err := mockDep.GetUser(context.TODO(), tt.req) assert.EqualValues(t, resp, tt.want) assert.EqualValues(t, err, tt.err) @@ -398,7 +519,7 @@ func TestGetCurrentUser(t *testing.T) { email := "user@gotocompany.com" table := []struct { title string - setup func(ctx context.Context, us *mocks.UserService) context.Context + setup func(ctx context.Context, us *mocks.UserService, sd *mocks.ServiceDataService) context.Context header string want *shieldv1beta1.GetCurrentUserResponse err error @@ -410,7 +531,7 @@ func TestGetCurrentUser(t *testing.T) { }, { title: "should return not found error if user does not exist", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { + setup: func(ctx context.Context, us *mocks.UserService, sd *mocks.ServiceDataService) context.Context { us.EXPECT().GetByEmail(mock.AnythingOfType("*context.valueCtx"), email).Return(user.User{}, user.ErrNotExist) return user.SetContextWithEmail(ctx, email) }, @@ -419,7 +540,7 @@ func TestGetCurrentUser(t *testing.T) { }, { title: "should return error if user service return some error", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { + setup: func(ctx context.Context, us *mocks.UserService, sd *mocks.ServiceDataService) context.Context { us.EXPECT().GetByEmail(mock.AnythingOfType("*context.valueCtx"), email).Return(user.User{}, errors.New("some error")) return user.SetContextWithEmail(ctx, email) }, @@ -428,19 +549,32 @@ func TestGetCurrentUser(t *testing.T) { }, { title: "should return user if user service return nil error", - setup: func(ctx context.Context, us *mocks.UserService) context.Context { - us.EXPECT().GetByEmail(mock.AnythingOfType("*context.valueCtx"), email).Return( + setup: func(ctx context.Context, us *mocks.UserService, sd *mocks.ServiceDataService) context.Context { + ctx = user.SetContextWithEmail(ctx, email) + us.EXPECT().GetByEmail(ctx, email).Return( user.User{ - ID: "user-id-1", - Name: "some user", - Email: "someuser@test.com", - Metadata: metadata.Metadata{ - "foo": "bar", - }, + ID: "user-id-1", + Name: "some user", + Email: "someuser@test.com", CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, nil) - return user.SetContextWithEmail(ctx, email) + + sd.EXPECT().Get(ctx, servicedata.Filter{ + ID: "user-id-1", + Namespace: userNamespaceID, + Entities: maps.Values(map[string]string{ + "user": userNamespaceID, + }), + }).Return([]servicedata.ServiceData{ + { + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, + }, nil) + return ctx }, want: &shieldv1beta1.GetCurrentUserResponse{User: &shieldv1beta1.User{ Id: "user-id-1", @@ -461,11 +595,12 @@ func TestGetCurrentUser(t *testing.T) { for _, tt := range table { t.Run(tt.title, func(t *testing.T) { mockUserSrv := new(mocks.UserService) + mockServiceDataSrv := new(mocks.ServiceDataService) ctx := context.TODO() if tt.setup != nil { - ctx = tt.setup(ctx, mockUserSrv) + ctx = tt.setup(ctx, mockUserSrv, mockServiceDataSrv) } - mockDep := Handler{userService: mockUserSrv} + mockDep := Handler{userService: mockUserSrv, serviceDataService: mockServiceDataSrv} resp, err := mockDep.GetCurrentUser(ctx, nil) assert.EqualValues(t, resp, tt.want) assert.EqualValues(t, err, tt.err) @@ -477,7 +612,7 @@ func TestUpdateUser(t *testing.T) { someID := uuid.NewString() table := []struct { title string - setup func(us *mocks.UserService) + setup func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) req *shieldv1beta1.UpdateUserRequest header string want *shieldv1beta1.UpdateUserResponse @@ -485,14 +620,23 @@ func TestUpdateUser(t *testing.T) { }{ { title: "should return internal error if user service return some error", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) + sds.EXPECT().GetKeyByURN(mock.AnythingOfType("context.todoCtx"), "system:servicedata_key:foo").Return(servicedata.Key{ + ResourceID: "dba6ad2c-89cd-4021-bd0f-e6845637d731", + }, nil) + rs.EXPECT().CheckPermission(mock.AnythingOfType("context.todoCtx"), user.User{ + Email: "user2@gotocompany.com", + }, + namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, + "dba6ad2c-89cd-4021-bd0f-e6845637d731", + action.Action{ID: schema.EditPermission}).Return(true, nil) us.EXPECT().UpdateByID(mock.AnythingOfType("context.todoCtx"), user.User{ ID: someID, Name: "abc user", Email: "user@gotocompany.com", - Metadata: metadata.Metadata{ - "foo": "bar", - }, }).Return(user.User{}, errors.New("some error")) }, req: &shieldv1beta1.UpdateUserRequest{ @@ -512,7 +656,8 @@ func TestUpdateUser(t *testing.T) { }, { title: "should return not found error if id is invalid", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) us.EXPECT().UpdateByID(mock.AnythingOfType("context.todoCtx"), user.User{ Name: "abc user", Email: "user@gotocompany.com", @@ -537,14 +682,23 @@ func TestUpdateUser(t *testing.T) { }, { title: "should return already exist error if user service return error conflict", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) + sds.EXPECT().GetKeyByURN(mock.AnythingOfType("context.todoCtx"), "system:servicedata_key:foo").Return(servicedata.Key{ + ResourceID: "dba6ad2c-89cd-4021-bd0f-e6845637d731", + }, nil) + rs.EXPECT().CheckPermission(mock.AnythingOfType("context.todoCtx"), user.User{ + Email: "user2@gotocompany.com", + }, + namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, + "dba6ad2c-89cd-4021-bd0f-e6845637d731", + action.Action{ID: schema.EditPermission}).Return(true, nil) us.EXPECT().UpdateByID(mock.AnythingOfType("context.todoCtx"), user.User{ ID: someID, Name: "abc user", Email: "user@gotocompany.com", - Metadata: metadata.Metadata{ - "foo": "bar", - }, }).Return(user.User{}, user.ErrConflict) }, req: &shieldv1beta1.UpdateUserRequest{ @@ -564,7 +718,8 @@ func TestUpdateUser(t *testing.T) { }, { title: "should return bad request error if email in request empty", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{}, nil) us.EXPECT().UpdateByID(mock.AnythingOfType("context.todoCtx"), user.User{ ID: someID, Name: "abc user", @@ -589,7 +744,10 @@ func TestUpdateUser(t *testing.T) { }, { title: "should return invalid email error if email is invalid", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) }, req: &shieldv1beta1.UpdateUserRequest{ Id: someID, @@ -608,20 +766,87 @@ func TestUpdateUser(t *testing.T) { }, { title: "should return bad request error if empty request body", - req: &shieldv1beta1.UpdateUserRequest{Id: someID, Body: nil}, - want: nil, - err: grpcBadBodyError, + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) + }, + req: &shieldv1beta1.UpdateUserRequest{Id: someID, Body: nil}, + want: nil, + err: grpcBadBodyError, }, { - title: "should return success if user service return nil error", - setup: func(us *mocks.UserService) { + title: "should return error if servicedata service return error", + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) + sds.EXPECT().GetKeyByURN(mock.AnythingOfType("context.todoCtx"), "system:servicedata_key:foo").Return(servicedata.Key{ + ResourceID: "dba6ad2c-89cd-4021-bd0f-e6845637d731", + }, nil) + rs.EXPECT().CheckPermission(mock.AnythingOfType("context.todoCtx"), user.User{ + Email: "user2@gotocompany.com", + }, + namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, + "dba6ad2c-89cd-4021-bd0f-e6845637d731", + action.Action{ID: schema.EditPermission}).Return(true, nil) us.EXPECT().UpdateByID(mock.AnythingOfType("context.todoCtx"), user.User{ ID: someID, Name: "abc user", Email: "user@gotocompany.com", - Metadata: metadata.Metadata{ - "foo": "bar", + }).Return( + user.User{ + ID: someID, + Name: "abc user", + Email: "user@gotocompany.com", + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + }, nil) + + sds.EXPECT().Upsert(mock.AnythingOfType("context.todoCtx"), servicedata.ServiceData{ + EntityID: someID, + NamespaceID: userNamespaceID, + Key: servicedata.Key{ + Name: "foo", + ProjectID: "system", }, + Value: "bar", + }).Return(servicedata.ServiceData{}, errorsPkg.ErrForbidden) + }, + req: &shieldv1beta1.UpdateUserRequest{ + Id: someID, + Body: &shieldv1beta1.UserRequestBody{ + Name: "abc user", + Email: "user@gotocompany.com", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + }, + }, + want: nil, + err: status.Error(codes.PermissionDenied, "you are not authorized to update foo key"), + }, + { + title: "should be successful if user and servicedata service return nil error", + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) + sds.EXPECT().GetKeyByURN(mock.AnythingOfType("context.todoCtx"), "system:servicedata_key:foo").Return(servicedata.Key{ + ResourceID: "dba6ad2c-89cd-4021-bd0f-e6845637d731", + }, nil) + rs.EXPECT().CheckPermission(mock.AnythingOfType("context.todoCtx"), user.User{ + Email: "user2@gotocompany.com", + }, + namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, + "dba6ad2c-89cd-4021-bd0f-e6845637d731", + action.Action{ID: schema.EditPermission}).Return(true, nil) + us.EXPECT().UpdateByID(mock.AnythingOfType("context.todoCtx"), user.User{ + ID: someID, + Name: "abc user", + Email: "user@gotocompany.com", }).Return( user.User{ ID: someID, @@ -633,6 +858,21 @@ func TestUpdateUser(t *testing.T) { CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, nil) + + sds.EXPECT().Upsert(mock.AnythingOfType("context.todoCtx"), servicedata.ServiceData{ + EntityID: someID, + NamespaceID: userNamespaceID, + Key: servicedata.Key{ + Name: "foo", + ProjectID: "system", + }, + Value: "bar", + }).Return(servicedata.ServiceData{ + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, nil) }, req: &shieldv1beta1.UpdateUserRequest{ Id: someID, @@ -662,13 +902,22 @@ func TestUpdateUser(t *testing.T) { }, { title: "should return success even though name is empty", - setup: func(us *mocks.UserService) { + setup: func(us *mocks.UserService, sds *mocks.ServiceDataService, rs *mocks.RelationService) { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) + sds.EXPECT().GetKeyByURN(mock.AnythingOfType("context.todoCtx"), "system:servicedata_key:foo").Return(servicedata.Key{ + ResourceID: "dba6ad2c-89cd-4021-bd0f-e6845637d731", + }, nil) + rs.EXPECT().CheckPermission(mock.AnythingOfType("context.todoCtx"), user.User{ + Email: "user2@gotocompany.com", + }, + namespace.Namespace{ID: schema.ServiceDataKeyNamespace}, + "dba6ad2c-89cd-4021-bd0f-e6845637d731", + action.Action{ID: schema.EditPermission}).Return(true, nil) us.EXPECT().UpdateByID(mock.AnythingOfType("context.todoCtx"), user.User{ ID: someID, Email: "user@gotocompany.com", - Metadata: metadata.Metadata{ - "foo": "bar", - }, }).Return( user.User{ ID: someID, @@ -679,6 +928,21 @@ func TestUpdateUser(t *testing.T) { CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, nil) + + sds.EXPECT().Upsert(mock.AnythingOfType("context.todoCtx"), servicedata.ServiceData{ + EntityID: someID, + NamespaceID: userNamespaceID, + Key: servicedata.Key{ + Name: "foo", + ProjectID: "system", + }, + Value: "bar", + }).Return(servicedata.ServiceData{ + Key: servicedata.Key{ + Name: "foo", + }, + Value: "bar", + }, nil) }, req: &shieldv1beta1.UpdateUserRequest{ Id: someID, @@ -709,11 +973,18 @@ func TestUpdateUser(t *testing.T) { for _, tt := range table { t.Run(tt.title, func(t *testing.T) { mockUserSrv := new(mocks.UserService) + mockServiceDataSrv := new(mocks.ServiceDataService) + mockRelationSrv := new(mocks.RelationService) ctx := context.TODO() if tt.setup != nil { - tt.setup(mockUserSrv) + tt.setup(mockUserSrv, mockServiceDataSrv, mockRelationSrv) + } + mockDep := Handler{ + userService: mockUserSrv, serviceDataService: mockServiceDataSrv, serviceDataConfig: ServiceDataConfig{ + DefaultServiceDataProject: "system", + }, + relationService: mockRelationSrv, } - mockDep := Handler{userService: mockUserSrv} resp, err := mockDep.UpdateUser(ctx, tt.req) assert.EqualValues(t, resp, tt.want) assert.EqualValues(t, tt.err, err) @@ -813,6 +1084,9 @@ func TestUpdateCurrentUser(t *testing.T) { { title: "should return bad request error if empty request body", setup: func(ctx context.Context, us *mocks.UserService) context.Context { + us.EXPECT().FetchCurrentUser(mock.AnythingOfType("context.todoCtx")).Return(user.User{ + Email: "user2@gotocompany.com", + }, nil) return user.SetContextWithEmail(ctx, email) }, req: &shieldv1beta1.UpdateCurrentUserRequest{Body: nil}, diff --git a/internal/api/v1beta1/v1beta1.go b/internal/api/v1beta1/v1beta1.go index 18457504a..b5f8ee50b 100644 --- a/internal/api/v1beta1/v1beta1.go +++ b/internal/api/v1beta1/v1beta1.go @@ -14,7 +14,8 @@ type RelationTransformer interface { } type ServiceDataConfig struct { - MaxUpsert int + MaxUpsert int + DefaultServiceDataProject string } type Handler struct { diff --git a/internal/schema/predefined.go b/internal/schema/predefined.go index b90cf6a07..6c29fb4d2 100644 --- a/internal/schema/predefined.go +++ b/internal/schema/predefined.go @@ -29,8 +29,9 @@ const ( MembershipPermission = "membership" // principals - UserPrincipal = "shield/user" - GroupPrincipal = "shield/group" + UserPrincipal = "shield/user" + GroupPrincipal = "shield/group" + UserPrincipalWildcard = "shield/user:*" ) var InheritedRelations = map[string]bool{ @@ -131,7 +132,7 @@ var ServiceDataKeyConfig = NamespaceConfig{ }, Roles: map[string][]string{ EditorRole: {UserPrincipal, GroupPrincipal}, - ViewerRole: {UserPrincipal, GroupPrincipal}, + ViewerRole: {UserPrincipal, GroupPrincipal, UserPrincipalWildcard}, OwnerRole: {UserPrincipal, GroupPrincipal}, }, Permissions: map[string][]string{ diff --git a/internal/server/config.go b/internal/server/config.go index a45ee2dfd..0e70ca3a6 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -13,8 +13,9 @@ type GRPCConfig struct { } type ServiceDataConfig struct { - BootstrapEnabled bool `yaml:"bootstrap_enabled" mapstructure:"bootstrap_enabled" default:"true"` - MaxNumUpsertData int `yaml:"max_num_upsert_data" mapstructure:"max_num_upsert_data" default:"1"` + BootstrapEnabled bool `yaml:"bootstrap_enabled" mapstructure:"bootstrap_enabled" default:"true"` + MaxNumUpsertData int `yaml:"max_num_upsert_data" mapstructure:"max_num_upsert_data" default:"1"` + DefaultServiceDataProject string `yaml:"default_service_data_project" mapstructure:"default_service_data_project" default:"system"` } func (cfg Config) grpcAddr() string { return fmt.Sprintf("%s:%d", cfg.Host, cfg.GRPC.Port) } diff --git a/internal/server/server.go b/internal/server/server.go index 2da0c76b5..9bca8a0d9 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -87,7 +87,7 @@ func Serve( healthHandler := health.NewHandler() grpc_health_v1.RegisterHealthServer(grpcServer, healthHandler) - serviceDataConfig := v1beta1.ServiceDataConfig{MaxUpsert: cfg.ServiceData.MaxNumUpsertData} + serviceDataConfig := v1beta1.ServiceDataConfig{MaxUpsert: cfg.ServiceData.MaxNumUpsertData, DefaultServiceDataProject: cfg.ServiceData.DefaultServiceDataProject} err = v1beta1.Register(ctx, grpcServer, deps, cfg.CheckAPILimit, serviceDataConfig) if err != nil { return err diff --git a/internal/store/postgres/group.go b/internal/store/postgres/group.go index a7f72767b..09df57b9c 100644 --- a/internal/store/postgres/group.go +++ b/internal/store/postgres/group.go @@ -2,7 +2,6 @@ package postgres import ( "database/sql" - "encoding/json" "time" "github.com/goto/shield/core/group" @@ -20,17 +19,12 @@ type Group struct { } func (from Group) transformToGroup() (group.Group, error) { - var unmarshalledMetadata map[string]any - if err := json.Unmarshal(from.Metadata, &unmarshalledMetadata); err != nil { - return group.Group{}, err - } - return group.Group{ ID: from.ID, Name: from.Name, Slug: from.Slug, OrganizationID: from.OrgID, - Metadata: unmarshalledMetadata, + Metadata: nil, CreatedAt: from.CreatedAt, UpdatedAt: from.UpdatedAt, }, nil diff --git a/internal/store/postgres/group_repository.go b/internal/store/postgres/group_repository.go index 17a8b6029..412b4801b 100644 --- a/internal/store/postgres/group_repository.go +++ b/internal/store/postgres/group_repository.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/doug-martin/goqu/v9" "github.com/goto/shield/core/group" @@ -25,6 +26,17 @@ type GroupRepository struct { dbc *db.Client } +type joinGroupMetadata struct { + ID string `db:"id"` + Name string `db:"name"` + Slug string `db:"slug"` + OrgId string `db:"org_id"` + Key any `db:"key"` + Value sql.NullString `db:"value"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + func NewGroupRepository(dbc *db.Client) *GroupRepository { return &GroupRepository{ dbc: dbc, @@ -208,11 +220,6 @@ func (r GroupRepository) Create(ctx context.Context, grp group.Group) (group.Gro return group.Group{}, group.ErrInvalidDetail } - marshaledMetadata, err := json.Marshal(grp.Metadata) - if err != nil { - return group.Group{}, fmt.Errorf("%w: %s", parseErr, err) - } - ctx = otelsql.WithCustomAttributes( ctx, []attribute.KeyValue{ @@ -226,7 +233,7 @@ func (r GroupRepository) Create(ctx context.Context, grp group.Group) (group.Gro "name": grp.Name, "slug": grp.Slug, "org_id": grp.OrganizationID, - "metadata": marshaledMetadata, + "metadata": nil, }).Returning(&Group{}).ToSQL() if err != nil { return group.Group{}, fmt.Errorf("%w: %s", queryErr, err) @@ -269,7 +276,44 @@ func (r GroupRepository) Create(ctx context.Context, grp group.Group) (group.Gro } func (r GroupRepository) List(ctx context.Context, flt group.Filter) ([]group.Group, error) { - sqlStatement := dialect.From(TABLE_GROUPS) + sqlStatement := dialect.From(TABLE_GROUPS).Select( + goqu.I("id"), + goqu.I("name"), + goqu.I("slug"), + goqu.I("org_id"), + goqu.I("created_at"), + goqu.I("updated_at"), + ) + + if len(flt.ServicedataKeyResourceIDs) > 0 { + subquery := dialect.Select( + goqu.I("sd.namespace_id"), + goqu.I("sd.entity_id"), + goqu.I("sk.name").As("name"), + goqu.I("sd.value"), + goqu.I("sk.resource_id"), + ).From(goqu.T(TABLE_SERVICE_DATA_KEYS).As("sk")). + RightJoin(goqu.T(TABLE_SERVICE_DATA).As("sd"), goqu.On( + goqu.I("sk.id").Eq(goqu.I("sd.key_id")))). + Where(goqu.Ex{"sd.namespace_id": schema.GroupPrincipal}, + goqu.Ex{"sk.project_id": flt.ProjectID}, + goqu.L( + "sk.resource_id", + ).In(flt.ServicedataKeyResourceIDs)) + + sqlStatement = dialect.Select( + goqu.I("g.id"), + goqu.I("g.name"), + goqu.I("g.slug"), + goqu.I("g.org_id"), + goqu.I("sd.name").As("key"), + goqu.I("sd.value"), + goqu.I("g.created_at"), + goqu.I("g.updated_at"), + ).From(goqu.T(TABLE_GROUPS).As("g")).LeftJoin(subquery.As("sd"), goqu.On( + goqu.Cast(goqu.C("id"), "TEXT").Eq(goqu.I("sd.entity_id")))) + } + if flt.OrganizationID != "" { sqlStatement = sqlStatement.Where(goqu.Ex{"org_id": flt.OrganizationID}) } @@ -286,7 +330,7 @@ func (r GroupRepository) List(ctx context.Context, flt group.Filter) ([]group.Gr }..., ) - var fetchedGroups []Group + var fetchedJoinGroupMetadata []joinGroupMetadata if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { nrCtx := newrelic.FromContext(ctx) if nrCtx != nil { @@ -299,7 +343,7 @@ func (r GroupRepository) List(ctx context.Context, flt group.Filter) ([]group.Gr defer nr.End() } - return r.dbc.SelectContext(ctx, &fetchedGroups, query, params...) + return r.dbc.SelectContext(ctx, &fetchedJoinGroupMetadata, query, params...) }); err != nil { err = checkPostgresError(err) switch { @@ -312,13 +356,39 @@ func (r GroupRepository) List(ctx context.Context, flt group.Filter) ([]group.Gr } } - var transformedGroups []group.Group - for _, v := range fetchedGroups { - transformedGroup, err := v.transformToGroup() - if err != nil { - return []group.Group{}, fmt.Errorf("%w: %s", parseErr, err) + groupedMetadataByGroup := make(map[string]group.Group) + for _, g := range fetchedJoinGroupMetadata { + if _, ok := groupedMetadataByGroup[g.ID]; !ok { + groupedMetadataByGroup[g.ID] = group.Group{} } - transformedGroups = append(transformedGroups, transformedGroup) + currentGroup := groupedMetadataByGroup[g.ID] + currentGroup.ID = g.ID + currentGroup.Slug = g.Slug + currentGroup.Name = g.Name + currentGroup.OrganizationID = g.OrgId + currentGroup.CreatedAt = g.CreatedAt + currentGroup.UpdatedAt = g.UpdatedAt + + if currentGroup.Metadata == nil { + currentGroup.Metadata = make(map[string]any) + } + + if g.Key != nil { + var value any + err := json.Unmarshal([]byte(g.Value.String), &value) + if err != nil { + continue + } + + currentGroup.Metadata[g.Key.(string)] = value + } + + groupedMetadataByGroup[g.ID] = currentGroup + } + + var transformedGroups []group.Group + for _, group := range groupedMetadataByGroup { + transformedGroups = append(transformedGroups, group) } return transformedGroups, nil @@ -333,17 +403,11 @@ func (r GroupRepository) UpdateByID(ctx context.Context, grp group.Group) (group return group.Group{}, group.ErrInvalidDetail } - marshaledMetadata, err := json.Marshal(grp.Metadata) - if err != nil { - return group.Group{}, fmt.Errorf("%w: %s", parseErr, err) - } - query, params, err := dialect.Update(TABLE_GROUPS).Set( goqu.Record{ "name": grp.Name, "slug": grp.Slug, "org_id": grp.OrganizationID, - "metadata": marshaledMetadata, "updated_at": goqu.L("now()"), }).Where(goqu.ExOr{ "id": grp.ID, @@ -399,16 +463,10 @@ func (r GroupRepository) UpdateBySlug(ctx context.Context, grp group.Group) (gro return group.Group{}, group.ErrInvalidDetail } - marshaledMetadata, err := json.Marshal(grp.Metadata) - if err != nil { - return group.Group{}, fmt.Errorf("%w: %s", parseErr, err) - } - query, params, err := dialect.Update(TABLE_GROUPS).Set( goqu.Record{ "name": grp.Name, "org_id": grp.OrganizationID, - "metadata": marshaledMetadata, "updated_at": goqu.L("now()"), }).Where(goqu.Ex{ "slug": grp.Slug, diff --git a/internal/store/postgres/group_repository_test.go b/internal/store/postgres/group_repository_test.go index 70bbcee93..b959e3755 100644 --- a/internal/store/postgres/group_repository_test.go +++ b/internal/store/postgres/group_repository_test.go @@ -3,6 +3,7 @@ package postgres_test import ( "context" "fmt" + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -21,6 +22,12 @@ import ( "github.com/goto/shield/pkg/db" ) +type SortByName []group.Group + +func (a SortByName) Len() int { return len(a) } +func (a SortByName) Less(i, j int) bool { return a[i].Name < a[j].Name } +func (a SortByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + type GroupRepositoryTestSuite struct { suite.Suite ctx context.Context @@ -429,6 +436,9 @@ func (s *GroupRepositoryTestSuite) TestList() { s.T().Fatalf("got error %s, expected was %s", err.Error(), tc.ErrString) } } + + sort.Sort(SortByName(got)) + if !cmp.Equal(got, tc.ExpectedGroups, cmpopts.IgnoreFields(group.Group{}, "ID", "Metadata", "CreatedAt", "UpdatedAt")) { s.T().Fatalf("got result %+v, expected was %+v", got, tc.ExpectedGroups) } diff --git a/internal/store/postgres/migrations/20240614092110_alter_value_servicedata_column.down.sql b/internal/store/postgres/migrations/20240614092110_alter_value_servicedata_column.down.sql new file mode 100644 index 000000000..e09560817 --- /dev/null +++ b/internal/store/postgres/migrations/20240614092110_alter_value_servicedata_column.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE servicedata ALTER COLUMN value TYPE varchar USING (value::varchar); + +UPDATE servicedata SET value = SUBSTRING(value, 2, LENGTH(value) - 2) WHERE value LIKE '"%"' AND (value NOT LIKE '[%' AND value NOT LIKE '{%'); diff --git a/internal/store/postgres/migrations/20240614092110_alter_value_servicedata_column.up.sql b/internal/store/postgres/migrations/20240614092110_alter_value_servicedata_column.up.sql new file mode 100644 index 000000000..bbafe775a --- /dev/null +++ b/internal/store/postgres/migrations/20240614092110_alter_value_servicedata_column.up.sql @@ -0,0 +1,3 @@ +UPDATE servicedata SET value = CONCAT('"',value,'"') where value NOT LIKE '[%' AND value NOT LIKE '{%' AND value NOT like '"%"' AND value NOT LIKE '%[^0-9.]%'; + +ALTER TABLE servicedata ALTER COLUMN value TYPE jsonb USING (value::jsonb); \ No newline at end of file diff --git a/internal/store/postgres/migrations/20240615121041_rename_key_to_name.down.sql b/internal/store/postgres/migrations/20240615121041_rename_key_to_name.down.sql new file mode 100644 index 000000000..9b26d253e --- /dev/null +++ b/internal/store/postgres/migrations/20240615121041_rename_key_to_name.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS servicedata_keys +RENAME COLUMN name TO key; \ No newline at end of file diff --git a/internal/store/postgres/migrations/20240615121041_rename_key_to_name.up.sql b/internal/store/postgres/migrations/20240615121041_rename_key_to_name.up.sql new file mode 100644 index 000000000..55154ef92 --- /dev/null +++ b/internal/store/postgres/migrations/20240615121041_rename_key_to_name.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS servicedata_keys +RENAME COLUMN key TO name; \ No newline at end of file diff --git a/internal/store/postgres/postgres_test.go b/internal/store/postgres/postgres_test.go index a240c0a5c..de7584aeb 100644 --- a/internal/store/postgres/postgres_test.go +++ b/internal/store/postgres/postgres_test.go @@ -528,7 +528,7 @@ func bootstrapServiceDataKey(client *db.Client, resources []resource.Resource, p for i, d := range data { d.ProjectID = projects[i].ID d.ResourceID = resources[i].Idxa - d.URN = d.CreateURN() + d.URN = servicedata.CreateURN(d.ProjectSlug, d.Name) insertedKey, err := serviceDataRepository.CreateKey(context.Background(), d) if err != nil { diff --git a/internal/store/postgres/role_repository.go b/internal/store/postgres/role_repository.go index e9a38e7ff..d0d5fe57b 100644 --- a/internal/store/postgres/role_repository.go +++ b/internal/store/postgres/role_repository.go @@ -116,7 +116,8 @@ func (r RoleRepository) Upsert(ctx context.Context, rl role.Role) (string, error "metadata": goqu.L("$5"), }).OnConflict( goqu.DoUpdate("id", goqu.Record{ - "name": goqu.L("$2"), + "types": goqu.L("$3"), + "metadata": goqu.L("$5"), }, )).Returning("id").ToSQL() if err != nil { diff --git a/internal/store/postgres/servicedata.go b/internal/store/postgres/servicedata.go index 12095c17a..ee5204886 100644 --- a/internal/store/postgres/servicedata.go +++ b/internal/store/postgres/servicedata.go @@ -2,6 +2,7 @@ package postgres import ( "database/sql" + "encoding/json" "time" "github.com/goto/shield/core/servicedata" @@ -11,7 +12,7 @@ type Key struct { ID string `db:"id"` URN string `db:"urn"` ProjectID string `db:"project_id"` - Key string `db:"key"` + Name string `db:"name"` Description string `db:"description"` ResourceID string `db:"resource_id"` CreatedAt time.Time `db:"created_at"` @@ -20,26 +21,34 @@ type Key struct { } type ServiceData struct { - URN string `db:"urn"` - NamespaceID string `db:"namespace_id"` - EntityID string `db:"entity_id"` - Value string `db:"value"` - Key string `db:"key"` - ProjectID string `db:"project_id"` - ResourceID string `db:"resource_id"` + URN string `db:"urn"` + NamespaceID string `db:"namespace_id"` + EntityID string `db:"entity_id"` + Value sql.NullString `db:"value"` + KeyName string `db:"key"` + ProjectID string `db:"project_id"` + ResourceID string `db:"resource_id"` } func (from ServiceData) transformToServiceData() servicedata.ServiceData { + var value any + if from.KeyName != "" { + err := json.Unmarshal([]byte(from.Value.String), &value) + if err != nil { + return servicedata.ServiceData{} + } + } + return servicedata.ServiceData{ NamespaceID: from.NamespaceID, EntityID: from.EntityID, Key: servicedata.Key{ URN: from.URN, ProjectID: from.ProjectID, - Key: from.Key, + Name: from.KeyName, ResourceID: from.ResourceID, }, - Value: from.Value, + Value: value, } } @@ -48,7 +57,7 @@ func (from Key) transformToServiceDataKey() servicedata.Key { ID: from.ID, URN: from.URN, ProjectID: from.ProjectID, - Key: from.Key, + Name: from.Name, Description: from.Description, ResourceID: from.ResourceID, } diff --git a/internal/store/postgres/servicedata_repository.go b/internal/store/postgres/servicedata_repository.go index 9502cf395..377438593 100644 --- a/internal/store/postgres/servicedata_repository.go +++ b/internal/store/postgres/servicedata_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "encoding/json" "errors" "fmt" @@ -26,7 +27,7 @@ func NewServiceDataRepository(dbc *db.Client) *ServiceDataRepository { } func (r ServiceDataRepository) CreateKey(ctx context.Context, key servicedata.Key) (servicedata.Key, error) { - if len(key.Key) == 0 { + if len(key.Name) == 0 { return servicedata.Key{}, servicedata.ErrInvalidDetail } @@ -34,7 +35,7 @@ func (r ServiceDataRepository) CreateKey(ctx context.Context, key servicedata.Ke goqu.Record{ "urn": key.URN, "project_id": key.ProjectID, - "key": key.Key, + "name": key.Name, "description": key.Description, "resource_id": key.ResourceID, }).Returning(&Key{}).ToSQL() @@ -79,19 +80,24 @@ func (r ServiceDataRepository) CreateKey(ctx context.Context, key servicedata.Ke } func (r ServiceDataRepository) Upsert(ctx context.Context, data servicedata.ServiceData) (servicedata.ServiceData, error) { + valuejson, err := json.Marshal(data.Value) + if err != nil { + valuejson = []byte{} + } + query, params, err := dialect.Insert(TABLE_SERVICE_DATA).Rows( goqu.Record{ "namespace_id": data.NamespaceID, "entity_id": data.EntityID, "key_id": data.Key.ID, - "value": data.Value, + "value": valuejson, }, ).OnConflict(goqu.DoUpdate( "ON CONSTRAINT servicedata_namespace_id_entity_id_key_id_key", goqu.Record{ "key_id": data.Key.ID, - "value": data.Value, + "value": valuejson, }, - )).Returning("value", goqu.L(`?`, data.Key.Key).As("key")).ToSQL() + )).Returning("value", goqu.L(`?`, data.Key.Name).As("key")).ToSQL() if err != nil { return servicedata.ServiceData{}, queryErr } @@ -183,7 +189,7 @@ func (r ServiceDataRepository) Get(ctx context.Context, filter servicedata.Filte goqu.I("sk.resource_id"), goqu.I("sd.namespace_id"), goqu.I("sd.entity_id"), - goqu.I("sk.key"), + goqu.I("sk.name").As("key"), goqu.I("sd.value"), ).From(goqu.T(TABLE_SERVICE_DATA).As("sd")). Join(goqu.T(TABLE_SERVICE_DATA_KEYS).As("sk"), goqu.On( @@ -235,6 +241,18 @@ func (r ServiceDataRepository) Get(ctx context.Context, filter servicedata.Filte } } + sdMap := map[string]any{} + for _, sd := range serviceDataModel { + if sd.KeyName != "" { + var value any + err := json.Unmarshal([]byte(sd.Value.String), &value) + if err != nil { + continue + } + sdMap[sd.KeyName] = value + } + } + var transformedServiceData []servicedata.ServiceData for _, sdm := range serviceDataModel { sd := sdm.transformToServiceData() diff --git a/internal/store/postgres/servicedata_repository_test.go b/internal/store/postgres/servicedata_repository_test.go index 15da67ccc..88b4c2210 100644 --- a/internal/store/postgres/servicedata_repository_test.go +++ b/internal/store/postgres/servicedata_repository_test.go @@ -130,14 +130,14 @@ func (s *ServiceDataRepositoryTestSuite) TestCreateKey() { KeyToCreate: servicedata.Key{ URN: "test-urn", ProjectID: s.projects[0].ID, - Key: "test-key", + Name: "test-key", Description: "description for test-key", ResourceID: s.resources[0].Idxa, }, ExpectedKey: servicedata.Key{ URN: "test-urn", ProjectID: s.projects[0].ID, - Key: "test-key", + Name: "test-key", Description: "description for test-key", ResourceID: s.resources[0].Idxa, }, @@ -147,8 +147,8 @@ func (s *ServiceDataRepositoryTestSuite) TestCreateKey() { KeyToCreate: servicedata.Key{ URN: s.keys[0].URN, ProjectID: s.projects[0].ID, - Key: s.keys[0].Key, - Description: s.keys[0].Key, + Name: s.keys[0].Name, + Description: s.keys[0].Name, ResourceID: s.resources[0].Idxa, }, ErrString: servicedata.ErrConflict.Error(), @@ -158,7 +158,7 @@ func (s *ServiceDataRepositoryTestSuite) TestCreateKey() { KeyToCreate: servicedata.Key{ URN: "test-urn-00", ProjectID: "00000000-0000-0000-0000-000000000000", - Key: "test-key", + Name: "test-key", Description: "description for test-key", ResourceID: s.resources[0].Idxa, }, @@ -169,7 +169,7 @@ func (s *ServiceDataRepositoryTestSuite) TestCreateKey() { KeyToCreate: servicedata.Key{ URN: "test-urn-00", ProjectID: s.projects[0].ID, - Key: "test-key", + Name: "test-key", Description: "description for test-key", ResourceID: "00000000-0000-0000-0000-000000000000", }, @@ -180,7 +180,7 @@ func (s *ServiceDataRepositoryTestSuite) TestCreateKey() { KeyToCreate: servicedata.Key{ URN: "test-urn-00", ProjectID: s.projects[0].ID, - Key: "", + Name: "", Description: "description for test-key", ResourceID: s.resources[0].Idxa, }, @@ -228,7 +228,7 @@ func (s *ServiceDataRepositoryTestSuite) TestUpsert() { }, ExpectedServiceData: servicedata.ServiceData{ Key: servicedata.Key{ - Key: s.keys[0].Key, + Name: s.keys[0].Name, }, Value: testValue, }, @@ -304,7 +304,7 @@ func (s *ServiceDataRepositoryTestSuite) TestGet() { Key: servicedata.Key{ URN: s.keys[0].URN, ProjectID: s.keys[0].ProjectID, - Key: s.keys[0].Key, + Name: s.keys[0].Name, ResourceID: s.keys[0].ResourceID, }, Value: s.data[0].Value, diff --git a/internal/store/postgres/testdata/mock-servicedata-keys.json b/internal/store/postgres/testdata/mock-servicedata-keys.json index 368dd5b49..b7cb14c02 100644 --- a/internal/store/postgres/testdata/mock-servicedata-keys.json +++ b/internal/store/postgres/testdata/mock-servicedata-keys.json @@ -1,14 +1,14 @@ [ { - "key": "test-key-01", + "name": "test-key-01", "description": "description for test-key-01" }, { - "key": "test-key-02", + "name": "test-key-02", "description": "description for test-key-02" }, { - "key": "test-key-03", + "name": "test-key-03", "description": "description for test-key-03" } ] \ No newline at end of file diff --git a/internal/store/postgres/user_repository.go b/internal/store/postgres/user_repository.go index a1cdb0d7c..7dd47d948 100644 --- a/internal/store/postgres/user_repository.go +++ b/internal/store/postgres/user_repository.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "regexp" "strings" "time" @@ -18,6 +17,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.4.0" "github.com/goto/shield/core/user" + "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/db" "github.com/goto/shield/pkg/uuid" ) @@ -91,67 +91,10 @@ func (r UserRepository) GetByID(ctx context.Context, id string) (user.User, erro } } - metadataQuery, params, err := dialect.From(TABLE_METADATA).Select("key", "value"). - Where(goqu.Ex{ - "user_id": fetchedUser.ID, - }).ToSQL() - if err != nil { - return user.User{}, fmt.Errorf("%w: %s", queryErr, err) - } - - data := make(map[string]interface{}) - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "GetByUserID", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - metadata, err := r.dbc.QueryContext(ctx, metadataQuery) - if err != nil { - return err - } - - for { - var key string - var valuejson string - if !metadata.Next() { - break - } - err := metadata.Scan(&key, &valuejson) - if err != nil { - return err - } - var value any - err = json.Unmarshal([]byte(valuejson), &value) - if err != nil { - return err - } - data[key] = value - } - - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.User{}, user.ErrConflict - default: - return user.User{}, err - } - } - transformedUser, err := fetchedUser.transformToUser() if err != nil { return user.User{}, fmt.Errorf("%w: %s", parseErr, err) } - transformedUser.Metadata = data return transformedUser, nil } @@ -220,66 +163,11 @@ func (r UserRepository) Create(ctx context.Context, usr user.User) (user.User, e return user.User{}, fmt.Errorf("%w: %s", parseErr, err) } - var rows []interface{} - for k, v := range usr.Metadata { - valuejson, err := json.Marshal(v) - if err != nil { - valuejson = []byte{} - } - - rows = append(rows, goqu.Record{ - "user_id": transformedUser.ID, - "key": k, - "value": valuejson, - }) - } - metadataQuery, _, err := dialect.Insert(TABLE_METADATA).Rows(rows...).ToSQL() - if err != nil { - return user.User{}, err - } - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "Create", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - _, err := tx.ExecContext(ctx, metadataQuery, params...) - if err != nil { - return err - } - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.User{}, user.ErrConflict - case errors.Is(err, errForeignKeyViolation): - re := regexp.MustCompile(`\(([^)]+)\) `) - match := re.FindStringSubmatch(err.Error()) - if len(match) > 1 { - return user.User{}, fmt.Errorf("%w:%s", user.ErrKeyDoesNotExists, match[1]) - } - return user.User{}, user.ErrKeyDoesNotExists - - default: - tx.Rollback() - return user.User{}, err - } - } - err = tx.Commit() if err != nil { return user.User{}, err } - transformedUser.Metadata = usr.Metadata return transformedUser, nil } @@ -297,26 +185,66 @@ func (r UserRepository) List(ctx context.Context, flt user.Filter) ([]user.User, offset := (flt.Page - 1) * flt.Limit - query, params, err := dialect.From(TABLE_USERS).LeftOuterJoin( - goqu.T(TABLE_METADATA), - goqu.On(goqu.Ex{"users.id": goqu.I("metadata.user_id")})).Select("users.id", "name", "email", "key", "value", "users.created_at", "users.updated_at").Where( - goqu.I("users.email").In( - goqu.From("users"). - Select(goqu.DISTINCT("email")). - Where( - goqu.Or( - goqu.C("name").ILike(fmt.Sprintf("%%%s%%", flt.Keyword)), - goqu.C("email").ILike(fmt.Sprintf("%%%s%%", flt.Keyword)), - ), - ). - Limit(uint(flt.Limit)). - Offset(uint(offset)), + query, params, err := dialect.From(goqu.T(TABLE_USERS)).Select( + goqu.I("id"), + goqu.I("name"), + goqu.I("email"), + goqu.I("created_at"), + goqu.I("updated_at"), + ).Where( + goqu.Or( + goqu.C("name").ILike(fmt.Sprintf("%%%s%%", flt.Keyword)), + goqu.C("email").ILike(fmt.Sprintf("%%%s%%", flt.Keyword)), ), - ).ToSQL() + ).Limit(uint(flt.Limit)).Offset(uint(offset)).ToSQL() if err != nil { return []user.User{}, fmt.Errorf("%w: %s", queryErr, err) } + if len(flt.ServiceDataKeyResourceIds) > 0 { + subquery := dialect.Select( + goqu.I("sd.namespace_id"), + goqu.I("sd.entity_id"), + goqu.I("sk.name").As("name"), + goqu.I("sd.value"), + goqu.I("sk.resource_id"), + ).From(goqu.T(TABLE_SERVICE_DATA_KEYS).As("sk")). + RightJoin(goqu.T(TABLE_SERVICE_DATA).As("sd"), goqu.On( + goqu.I("sk.id").Eq(goqu.I("sd.key_id")))). + Where(goqu.Ex{"sd.namespace_id": schema.UserPrincipal}, + goqu.Ex{"sk.project_id": flt.ProjectID}, + goqu.L( + "sk.resource_id", + ).In(flt.ServiceDataKeyResourceIds)) + + query, params, err = dialect.Select( + goqu.I("u.id"), + goqu.I("u.name"), + goqu.I("u.email"), + goqu.I("sd.name").As("key"), + goqu.I("sd.value"), + goqu.I("u.created_at"), + goqu.I("u.updated_at"), + ).From(goqu.T(TABLE_USERS).As("u")).LeftJoin(subquery.As("sd"), goqu.On( + goqu.Cast(goqu.C("id"), "TEXT").Eq(goqu.I("sd.entity_id")))).Where( + goqu.I("u.email").In( + goqu.From(TABLE_USERS). + Select(goqu.DISTINCT("email")). + Where( + goqu.Or( + goqu.C("name").ILike(fmt.Sprintf("%%%s%%", flt.Keyword)), + goqu.C("email").ILike(fmt.Sprintf("%%%s%%", flt.Keyword)), + ), + ). + Limit(uint(flt.Limit)). + Offset(uint(offset)), + ), + ).ToSQL() + if err != nil { + return []user.User{}, fmt.Errorf("%w: %s", queryErr, err) + } + } + ctx = otelsql.WithCustomAttributes( ctx, []attribute.KeyValue{ @@ -440,8 +368,6 @@ func (r UserRepository) GetByIDs(ctx context.Context, userIDs []string) ([]user. } func (r UserRepository) UpdateByEmail(ctx context.Context, usr user.User) (user.User, error) { - userMetadata := make(map[string]any) - if strings.TrimSpace(usr.Email) == "" { return user.User{}, user.ErrInvalidEmail } @@ -503,157 +429,12 @@ func (r UserRepository) UpdateByEmail(ctx context.Context, usr user.User) (user. return fmt.Errorf("%s: %w", parseErr, err) } - if usr.Metadata != nil { - existingMetadataQuery, params, err := dialect.From(TABLE_METADATA).Select("key", "value"). - Where(goqu.Ex{ - "user_id": transformedUser.ID, - }).ToSQL() - if err != nil { - return fmt.Errorf("%w: %s", queryErr, err) - } - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "GetByUserID", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - metadata, err := r.dbc.QueryContext(ctx, existingMetadataQuery) - if err != nil { - return err - } - - for { - var key string - var valuejson string - if !metadata.Next() { - break - } - err := metadata.Scan(&key, &valuejson) - if err != nil { - return err - } - - var value any - err = json.Unmarshal([]byte(valuejson), &value) - if err != nil { - return err - } - - userMetadata[key] = value - } - - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.ErrConflict - default: - return err - } - } - - metadataDeleteQuery, params, err := dialect.Delete(TABLE_METADATA). - Where( - goqu.Ex{ - "user_id": transformedUser.ID, - }, - ).ToSQL() - if err != nil { - return nil - } - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "DeleteByUserID", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - _, err := tx.ExecContext(ctx, metadataDeleteQuery, params...) - if err != nil { - return err - } - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.ErrConflict - default: - return err - } - } - - for key, value := range usr.Metadata { - userMetadata[key] = value - } - - var rows []interface{} - for k, v := range userMetadata { - valuejson, err := json.Marshal(v) - if err != nil { - return err - } - rows = append(rows, goqu.Record{ - "user_id": transformedUser.ID, - "key": k, - "value": valuejson, - }) - } - metadataQuery, params, err := dialect.Insert(TABLE_METADATA).Rows(rows...).ToSQL() - if err != nil { - return err - } - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "Create", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - _, err := tx.ExecContext(ctx, metadataQuery, params...) - if err != nil { - return err - } - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.ErrConflict - default: - return err - } - } - } - return nil }) if err != nil { return user.User{}, err } - transformedUser.Metadata = userMetadata - return transformedUser, nil } @@ -726,98 +507,12 @@ func (r UserRepository) UpdateByID(ctx context.Context, usr user.User) (user.Use if err != nil { return fmt.Errorf("%s: %w", parseErr, err) } - - metadataDeleteQuery, params, err := dialect.Delete(TABLE_METADATA). - Where( - goqu.Ex{ - "user_id": transformedUser.ID, - }, - ).ToSQL() - if err != nil { - return nil - } - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "DeleteByUserID", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - _, err := tx.ExecContext(ctx, metadataDeleteQuery, params...) - if err != nil { - return err - } - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.ErrConflict - default: - return err - } - } - - if len(usr.Metadata) > 0 { - var rows []interface{} - - for k, v := range usr.Metadata { - valuejson, err := json.Marshal(v) - if err != nil { - valuejson = []byte{} - } - - rows = append(rows, goqu.Record{ - "user_id": transformedUser.ID, - "key": k, - "value": valuejson, - }) - } - metadataQuery, _, err := dialect.Insert(TABLE_METADATA).Rows(rows...).ToSQL() - if err != nil { - return err - } - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "Create", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - _, err := tx.ExecContext(ctx, metadataQuery, params...) - if err != nil { - return err - } - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.ErrConflict - default: - return err - } - } - } return nil }) if err != nil { return user.User{}, err } - transformedUser.Metadata = usr.Metadata return transformedUser, nil } @@ -827,7 +522,6 @@ func (r UserRepository) GetByEmail(ctx context.Context, email string) (user.User } var fetchedUser User - data := make(map[string]any) query, params, err := dialect.From(TABLE_USERS).Where( goqu.Ex{ @@ -865,67 +559,11 @@ func (r UserRepository) GetByEmail(ctx context.Context, email string) (user.User return user.User{}, fmt.Errorf("%w: %s", dbErr, err) } - metadataQuery, params, err := dialect.From(TABLE_METADATA).Select("key", "value"). - Where(goqu.Ex{ - "user_id": fetchedUser.ID, - }).ToSQL() - if err != nil { - return user.User{}, fmt.Errorf("%w: %s", queryErr, err) - } - - if err = r.dbc.WithTimeout(ctx, func(ctx context.Context) error { - nrCtx := newrelic.FromContext(ctx) - if nrCtx != nil { - nr := newrelic.DatastoreSegment{ - Product: newrelic.DatastorePostgres, - Collection: TABLE_METADATA, - Operation: "GetByUserID", - StartTime: nrCtx.StartSegmentNow(), - } - defer nr.End() - } - - metadata, err := r.dbc.QueryContext(ctx, metadataQuery) - if err != nil { - return err - } - - for { - var key string - var valuejson string - if !metadata.Next() { - break - } - err := metadata.Scan(&key, &valuejson) - if err != nil { - return err - } - var value any - err = json.Unmarshal([]byte(valuejson), &value) - if err != nil { - return err - } - data[key] = value - } - - return nil - }); err != nil { - err = checkPostgresError(err) - switch { - case errors.Is(err, errDuplicateKey): - return user.User{}, user.ErrConflict - default: - return user.User{}, err - } - } - transformedUser, err := fetchedUser.transformToUser() if err != nil { return user.User{}, fmt.Errorf("%w: %s", parseErr, err) } - transformedUser.Metadata = data - return transformedUser, nil } diff --git a/internal/store/postgres/user_repository_test.go b/internal/store/postgres/user_repository_test.go index c685162c1..5369b843f 100644 --- a/internal/store/postgres/user_repository_test.go +++ b/internal/store/postgres/user_repository_test.go @@ -10,14 +10,16 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/goto/salt/log" "github.com/ory/dockertest" "github.com/stretchr/testify/suite" + "github.com/goto/salt/log" + "github.com/goto/shield/core/project" + "github.com/goto/shield/core/resource" + "github.com/goto/shield/core/servicedata" "github.com/goto/shield/core/user" "github.com/goto/shield/internal/store/postgres" "github.com/goto/shield/pkg/db" - "github.com/goto/shield/pkg/metadata" ) type UserRepositoryTestSuite struct { @@ -27,6 +29,10 @@ type UserRepositoryTestSuite struct { pool *dockertest.Pool resource *dockertest.Resource repository *postgres.UserRepository + keys []servicedata.Key + projects []project.Project + resources []resource.Resource + data []servicedata.ServiceData users []user.User } @@ -54,6 +60,41 @@ func (s *UserRepositoryTestSuite) SetupTest() { if err != nil { s.T().Fatal(err) } + + namespaces, err := bootstrapNamespace(s.client) + if err != nil { + s.T().Fatal(err) + } + + _, err = bootstrapMetadataKeys(s.client) + if err != nil { + s.T().Fatal(err) + } + + organizations, err := bootstrapOrganization(s.client) + if err != nil { + s.T().Fatal(err) + } + + s.projects, err = bootstrapProject(s.client, organizations) + if err != nil { + s.T().Fatal(err) + } + + s.resources, err = bootstrapResource(s.client, s.projects, organizations, namespaces, s.users) + if err != nil { + s.T().Fatal(err) + } + + s.keys, err = bootstrapServiceDataKey(s.client, s.resources, s.projects) + if err != nil { + s.T().Fatal(err) + } + + s.data, err = bootstrapServiceData(s.client, s.users, s.keys) + if err != nil { + s.T().Fatal(err) + } } func (s *UserRepositoryTestSuite) TearDownSuite() { @@ -74,6 +115,11 @@ func (s *UserRepositoryTestSuite) cleanup() error { fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_METADATA), fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_USERS), fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_METADATA_KEYS), + fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_SERVICE_DATA_KEYS), + fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_SERVICE_DATA), + fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_ORGANIZATIONS), + fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_PROJECTS), + fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", postgres.TABLE_RESOURCES), } return execQueries(context.TODO(), s.client, queries) } @@ -282,8 +328,8 @@ func (s *UserRepositoryTestSuite) TestList() { }, ExpectedUsers: []user.User{ { - Name: s.users[3].Name, - Email: s.users[3].Email, + Name: s.users[0].Name, + Email: s.users[0].Email, }, }, }, @@ -421,16 +467,10 @@ func (s *UserRepositoryTestSuite) TestUpdateByEmail() { UserToUpdate: user.User{ Name: "Doe John", Email: s.users[0].Email, - Metadata: metadata.Metadata{ - "k1": "v1", - }, }, ExpectedUser: user.User{ Name: "Doe John", Email: s.users[0].Email, - Metadata: metadata.Metadata{ - "k1": "v1", - }, }, }, { @@ -481,17 +521,11 @@ func (s *UserRepositoryTestSuite) TestUpdateByID() { ID: s.users[0].ID, Name: "Doe John", Email: s.users[0].Email, - Metadata: metadata.Metadata{ - "k2": "v2", - }, }, ExpectedUser: user.User{ ID: s.users[0].ID, Name: "Doe John", Email: s.users[0].Email, - Metadata: metadata.Metadata{ - "k2": "v2", - }, }, }, { diff --git a/internal/store/spicedb/relation_repository.go b/internal/store/spicedb/relation_repository.go index 34ad66411..610942ce3 100644 --- a/internal/store/spicedb/relation_repository.go +++ b/internal/store/spicedb/relation_repository.go @@ -231,6 +231,11 @@ func (r RelationRepository) DeleteSubjectRelations(ctx context.Context, resource func (r RelationRepository) LookupResources(ctx context.Context, resourceType, permission, subjectType, subjectID string) ([]string, error) { request := &authzedpb.LookupResourcesRequest{ + Consistency: &authzedpb.Consistency{ + Requirement: &authzedpb.Consistency_FullyConsistent{ + FullyConsistent: true, + }, + }, ResourceObjectType: resourceType, Permission: permission, Subject: &authzedpb.SubjectReference{ diff --git a/internal/store/spicedb/schema_generator/generator.go b/internal/store/spicedb/schema_generator/generator.go index 7fb0ca6ab..4f1e6e35d 100644 --- a/internal/store/spicedb/schema_generator/generator.go +++ b/internal/store/spicedb/schema_generator/generator.go @@ -49,7 +49,8 @@ func GenerateSchema(namespaceConfig schema.NamespaceConfigMapType) []string { func processPrincipal(s string) string { return map[string]string{ - "shield/group": "shield/group#membership", - "shield/user": "shield/user", + "shield/group": "shield/group#membership", + "shield/user": "shield/user", + "shield/user:*": "shield/user:*", }[s] } diff --git a/internal/store/spicedb/schema_generator/generator_test.go b/internal/store/spicedb/schema_generator/generator_test.go index 87818c80b..102d53d70 100644 --- a/internal/store/spicedb/schema_generator/generator_test.go +++ b/internal/store/spicedb/schema_generator/generator_test.go @@ -33,5 +33,5 @@ func TestPredefinedSchema(t *testing.T) { schema.PreDefinedSystemNamespaceConfig[schema.ServiceDataKeyNamespace] = schema.ServiceDataKeyConfig actualPredefinedConfigs := makeDefnMap(GenerateSchema(schema.PreDefinedSystemNamespaceConfig)) expectedPredefinedConfigs := makeDefnMap(strings.Split(string(content), "\n--\n")) - assert.Equal(t, actualPredefinedConfigs, expectedPredefinedConfigs) + assert.Equal(t, expectedPredefinedConfigs, actualPredefinedConfigs) } diff --git a/internal/store/spicedb/schema_generator/predefined_schema b/internal/store/spicedb/schema_generator/predefined_schema index 63ba8020e..c2ae7cb96 100644 --- a/internal/store/spicedb/schema_generator/predefined_schema +++ b/internal/store/spicedb/schema_generator/predefined_schema @@ -30,7 +30,7 @@ definition shield/group { -- definition shield/servicedata_key { relation editor: shield/user | shield/group#membership - relation viewer: shield/user | shield/group#membership + relation viewer: shield/user | shield/group#membership | shield/user:* relation owner: shield/user | shield/group#membership permission edit = owner + editor + organization->owner + organization->editor + project->owner + project->editor permission view = owner + editor + viewer + organization->owner + organization->editor + organization->viewer + project->owner + project->editor + project->viewer diff --git a/proto/shield.swagger.yaml b/proto/shield.swagger.yaml index ff038afbe..d1b52fae4 100644 --- a/proto/shield.swagger.yaml +++ b/proto/shield.swagger.yaml @@ -1547,7 +1547,7 @@ definitions: `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. - The JSON representation for `NullValue` is JSON `null`. + The JSON representation for `NullValue` is JSON `null`. - NULL_VALUE: Null value. Organization: @@ -1821,8 +1821,6 @@ definitions: properties: data: type: object - additionalProperties: - type: string UpsertServiceDataRequestBody: type: object properties: @@ -1830,15 +1828,11 @@ definitions: type: string data: type: object - additionalProperties: - type: string UpsertUserServiceDataResponse: type: object properties: data: type: object - additionalProperties: - type: string User: type: object properties: diff --git a/proto/v1beta1/servicedata.pb.go b/proto/v1beta1/servicedata.pb.go index 195150cd6..c3bc6e9c0 100644 --- a/proto/v1beta1/servicedata.pb.go +++ b/proto/v1beta1/servicedata.pb.go @@ -241,8 +241,8 @@ type UpsertServiceDataRequestBody struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` - Data map[string]string `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + Data *structpb.Struct `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` } func (x *UpsertServiceDataRequestBody) Reset() { @@ -284,7 +284,7 @@ func (x *UpsertServiceDataRequestBody) GetProject() string { return "" } -func (x *UpsertServiceDataRequestBody) GetData() map[string]string { +func (x *UpsertServiceDataRequestBody) GetData() *structpb.Struct { if x != nil { return x.Data } @@ -406,7 +406,7 @@ type UpsertUserServiceDataResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Data map[string]string `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` } func (x *UpsertUserServiceDataResponse) Reset() { @@ -441,7 +441,7 @@ func (*UpsertUserServiceDataResponse) Descriptor() ([]byte, []int) { return file_gotocompany_shield_v1beta1_servicedata_proto_rawDescGZIP(), []int{7} } -func (x *UpsertUserServiceDataResponse) GetData() map[string]string { +func (x *UpsertUserServiceDataResponse) GetData() *structpb.Struct { if x != nil { return x.Data } @@ -453,7 +453,7 @@ type UpsertGroupServiceDataResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Data map[string]string `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` } func (x *UpsertGroupServiceDataResponse) Reset() { @@ -488,7 +488,7 @@ func (*UpsertGroupServiceDataResponse) Descriptor() ([]byte, []int) { return file_gotocompany_shield_v1beta1_servicedata_proto_rawDescGZIP(), []int{8} } -func (x *UpsertGroupServiceDataResponse) GetData() map[string]string { +func (x *UpsertGroupServiceDataResponse) GetData() *structpb.Struct { if x != nil { return x.Data } @@ -746,165 +746,146 @@ var file_gotocompany_shield_v1beta1_servicedata_proto_rawDesc = []byte{ 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x52, 0x0e, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x22, 0xc9, - 0x01, 0x0a, 0x1c, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, - 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x56, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, - 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, - 0x79, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x85, 0x01, 0x0a, 0x1c, 0x55, - 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, - 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, - 0x65, 0x72, 0x49, 0x64, 0x12, 0x4c, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, - 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, - 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, - 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, - 0x64, 0x79, 0x22, 0x88, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x22, 0x65, + 0x0a, 0x1c, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x18, + 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x85, 0x01, 0x0a, 0x1c, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x4c, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0xb1, 0x01, - 0x0a, 0x1d, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x57, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, - 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, - 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, - 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0xb3, 0x01, 0x0a, 0x1e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, - 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, - 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, - 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, - 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x7f, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x55, 0x73, - 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, - 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x17, 0xfa, - 0x42, 0x14, 0x92, 0x01, 0x11, 0x22, 0x0f, 0x72, 0x0d, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x52, - 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x18, - 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x51, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, - 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x49, 0x0a, 0x1a, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, + 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x88, 0x01, + 0x0a, 0x1d, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x4c, 0x0a, 0x04, 0x62, 0x6f, + 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, + 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, + 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x4c, 0x0a, 0x1d, 0x55, 0x70, 0x73, 0x65, + 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x4a, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x32, 0x96, 0x09, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, - 0x74, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xd7, 0x01, 0x0a, 0x14, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, - 0x65, 0x79, 0x12, 0x37, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, - 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, - 0x61, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x67, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, - 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4c, 0x92, 0x41, 0x27, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x12, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x20, 0x4b, 0x65, - 0x79, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x14, 0x2f, - 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, - 0x61, 0x74, 0x61, 0x12, 0xeb, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, - 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x38, 0x2e, - 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, - 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, - 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, - 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x5d, 0x92, 0x41, 0x28, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x20, 0x44, 0x61, 0x74, 0x61, 0x12, 0x18, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x20, 0x55, 0x73, - 0x65, 0x72, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x2c, 0x3a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x24, 0x2f, 0x76, 0x31, - 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, 0x61, 0x74, - 0x61, 0x12, 0xf1, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x39, 0x2e, 0x67, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, - 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, + 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x4d, 0x0a, 0x1e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, - 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7f, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x06, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x17, 0xfa, 0x42, 0x14, + 0x92, 0x01, 0x11, 0x22, 0x0f, 0x72, 0x0d, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x52, 0x05, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x51, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, + 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x49, 0x0a, 0x1a, 0x47, 0x65, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x22, 0x4a, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x60, 0x92, 0x41, 0x29, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x20, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, - 0x61, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2e, 0x3a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x26, 0x2f, - 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x64, 0x61, 0x74, 0x61, 0x12, 0xdd, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x35, 0x2e, 0x67, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, - 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, - 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, - 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x58, 0x92, 0x41, 0x29, - 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, - 0x47, 0x65, 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x20, 0x44, 0x61, 0x74, 0x61, 0x20, 0x4b, 0x65, 0x79, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x12, - 0x24, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, - 0x7b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x64, 0x61, 0x74, 0x61, 0x12, 0xe3, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x36, 0x2e, - 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, - 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, + 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x32, 0x96, 0x09, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xd7, 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, + 0x12, 0x37, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, + 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x67, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, + 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x4c, 0x92, 0x41, 0x27, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x12, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x20, 0x4b, 0x65, 0x79, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x14, 0x2f, 0x76, 0x31, + 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, 0x61, 0x74, + 0x61, 0x12, 0xeb, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x38, 0x2e, 0x67, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, + 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, + 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, + 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x5d, 0x92, 0x41, 0x28, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, + 0x61, 0x74, 0x61, 0x12, 0x18, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, + 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x2c, 0x3a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x24, 0x2f, 0x76, 0x31, 0x62, 0x65, + 0x74, 0x61, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, 0x61, 0x74, 0x61, 0x12, + 0xf1, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x39, 0x2e, 0x67, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, + 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, - 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5b, - 0x92, 0x41, 0x2a, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, - 0x61, 0x12, 0x1a, 0x47, 0x65, 0x74, 0x20, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x20, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x20, 0x4b, 0x65, 0x79, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x7d, 0x2f, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, 0x61, 0x74, 0x61, 0x42, 0x81, 0x01, 0x92, 0x41, - 0x1a, 0x12, 0x15, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, - 0x61, 0x32, 0x05, 0x30, 0x2e, 0x31, 0x2e, 0x30, 0x2a, 0x01, 0x01, 0x0a, 0x25, 0x63, 0x6f, 0x6d, - 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x6e, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, - 0x61, 0x31, 0x42, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x5a, - 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x74, 0x6f, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2f, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2f, 0x76, - 0x31, 0x3b, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x60, 0x92, 0x41, 0x29, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, + 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x20, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x2e, 0x3a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x26, 0x2f, 0x76, 0x31, + 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, + 0x61, 0x74, 0x61, 0x12, 0xdd, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x35, 0x2e, 0x67, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, + 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x36, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, + 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x58, 0x92, 0x41, 0x29, 0x0a, 0x0c, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, 0x47, 0x65, + 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, + 0x61, 0x74, 0x61, 0x20, 0x4b, 0x65, 0x79, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x12, 0x24, 0x2f, + 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, + 0x61, 0x74, 0x61, 0x12, 0xe3, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x36, 0x2e, 0x67, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, + 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, + 0x79, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5b, 0x92, 0x41, + 0x2a, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x1a, 0x47, 0x65, 0x74, 0x20, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x20, 0x4b, 0x65, 0x79, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x28, 0x12, 0x26, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x2f, 0x7b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x64, 0x61, 0x74, 0x61, 0x42, 0x81, 0x01, 0x92, 0x41, 0x1a, 0x12, + 0x15, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61, 0x32, + 0x05, 0x30, 0x2e, 0x31, 0x2e, 0x30, 0x2a, 0x01, 0x01, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x67, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x6e, 0x2e, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, + 0x42, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x5a, 0x2e, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x74, 0x6f, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2f, 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x2f, 0x76, 0x31, 0x3b, + 0x73, 0x68, 0x69, 0x65, 0x6c, 0x64, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -919,7 +900,7 @@ func file_gotocompany_shield_v1beta1_servicedata_proto_rawDescGZIP() []byte { return file_gotocompany_shield_v1beta1_servicedata_proto_rawDescData } -var file_gotocompany_shield_v1beta1_servicedata_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_gotocompany_shield_v1beta1_servicedata_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_gotocompany_shield_v1beta1_servicedata_proto_goTypes = []interface{}{ (*ServiceDataKeyRequestBody)(nil), // 0: gotocompany.shield.v1beta1.ServiceDataKeyRequestBody (*ServiceDataKey)(nil), // 1: gotocompany.shield.v1beta1.ServiceDataKey @@ -934,21 +915,18 @@ var file_gotocompany_shield_v1beta1_servicedata_proto_goTypes = []interface{}{ (*GetGroupServiceDataRequest)(nil), // 10: gotocompany.shield.v1beta1.GetGroupServiceDataRequest (*GetUserServiceDataResponse)(nil), // 11: gotocompany.shield.v1beta1.GetUserServiceDataResponse (*GetGroupServiceDataResponse)(nil), // 12: gotocompany.shield.v1beta1.GetGroupServiceDataResponse - nil, // 13: gotocompany.shield.v1beta1.UpsertServiceDataRequestBody.DataEntry - nil, // 14: gotocompany.shield.v1beta1.UpsertUserServiceDataResponse.DataEntry - nil, // 15: gotocompany.shield.v1beta1.UpsertGroupServiceDataResponse.DataEntry - (*structpb.Struct)(nil), // 16: google.protobuf.Struct + (*structpb.Struct)(nil), // 13: google.protobuf.Struct } var file_gotocompany_shield_v1beta1_servicedata_proto_depIdxs = []int32{ 0, // 0: gotocompany.shield.v1beta1.CreateServiceDataKeyRequest.body:type_name -> gotocompany.shield.v1beta1.ServiceDataKeyRequestBody 1, // 1: gotocompany.shield.v1beta1.CreateServiceDataKeyResponse.service_data_key:type_name -> gotocompany.shield.v1beta1.ServiceDataKey - 13, // 2: gotocompany.shield.v1beta1.UpsertServiceDataRequestBody.data:type_name -> gotocompany.shield.v1beta1.UpsertServiceDataRequestBody.DataEntry + 13, // 2: gotocompany.shield.v1beta1.UpsertServiceDataRequestBody.data:type_name -> google.protobuf.Struct 4, // 3: gotocompany.shield.v1beta1.UpsertUserServiceDataRequest.body:type_name -> gotocompany.shield.v1beta1.UpsertServiceDataRequestBody 4, // 4: gotocompany.shield.v1beta1.UpsertGroupServiceDataRequest.body:type_name -> gotocompany.shield.v1beta1.UpsertServiceDataRequestBody - 14, // 5: gotocompany.shield.v1beta1.UpsertUserServiceDataResponse.data:type_name -> gotocompany.shield.v1beta1.UpsertUserServiceDataResponse.DataEntry - 15, // 6: gotocompany.shield.v1beta1.UpsertGroupServiceDataResponse.data:type_name -> gotocompany.shield.v1beta1.UpsertGroupServiceDataResponse.DataEntry - 16, // 7: gotocompany.shield.v1beta1.GetUserServiceDataResponse.data:type_name -> google.protobuf.Struct - 16, // 8: gotocompany.shield.v1beta1.GetGroupServiceDataResponse.data:type_name -> google.protobuf.Struct + 13, // 5: gotocompany.shield.v1beta1.UpsertUserServiceDataResponse.data:type_name -> google.protobuf.Struct + 13, // 6: gotocompany.shield.v1beta1.UpsertGroupServiceDataResponse.data:type_name -> google.protobuf.Struct + 13, // 7: gotocompany.shield.v1beta1.GetUserServiceDataResponse.data:type_name -> google.protobuf.Struct + 13, // 8: gotocompany.shield.v1beta1.GetGroupServiceDataResponse.data:type_name -> google.protobuf.Struct 2, // 9: gotocompany.shield.v1beta1.ServiceDataService.CreateServiceDataKey:input_type -> gotocompany.shield.v1beta1.CreateServiceDataKeyRequest 5, // 10: gotocompany.shield.v1beta1.ServiceDataService.UpsertUserServiceData:input_type -> gotocompany.shield.v1beta1.UpsertUserServiceDataRequest 6, // 11: gotocompany.shield.v1beta1.ServiceDataService.UpsertGroupServiceData:input_type -> gotocompany.shield.v1beta1.UpsertGroupServiceDataRequest @@ -1135,7 +1113,7 @@ func file_gotocompany_shield_v1beta1_servicedata_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_gotocompany_shield_v1beta1_servicedata_proto_rawDesc, NumEnums: 0, - NumMessages: 16, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/v1beta1/servicedata.pb.validate.go b/proto/v1beta1/servicedata.pb.validate.go index 6ce62a4d8..9d6bd0969 100644 --- a/proto/v1beta1/servicedata.pb.validate.go +++ b/proto/v1beta1/servicedata.pb.validate.go @@ -535,7 +535,34 @@ func (m *UpsertServiceDataRequestBody) validate(all bool) error { // no validation rules for Project - // no validation rules for Data + if all { + switch v := interface{}(m.GetData()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, UpsertServiceDataRequestBodyValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, UpsertServiceDataRequestBodyValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetData()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return UpsertServiceDataRequestBodyValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + } + } + } if len(errors) > 0 { return UpsertServiceDataRequestBodyMultiError(errors) @@ -908,7 +935,34 @@ func (m *UpsertUserServiceDataResponse) validate(all bool) error { var errors []error - // no validation rules for Data + if all { + switch v := interface{}(m.GetData()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, UpsertUserServiceDataResponseValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, UpsertUserServiceDataResponseValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetData()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return UpsertUserServiceDataResponseValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + } + } + } if len(errors) > 0 { return UpsertUserServiceDataResponseMultiError(errors) @@ -1013,7 +1067,34 @@ func (m *UpsertGroupServiceDataResponse) validate(all bool) error { var errors []error - // no validation rules for Data + if all { + switch v := interface{}(m.GetData()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, UpsertGroupServiceDataResponseValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, UpsertGroupServiceDataResponseValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetData()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return UpsertGroupServiceDataResponseValidationError{ + field: "Data", + reason: "embedded message failed validation", + cause: err, + } + } + } if len(errors) > 0 { return UpsertGroupServiceDataResponseMultiError(errors) diff --git a/test/e2e_test/regression/api_test.go b/test/e2e_test/regression/api_test.go index 148c39c53..f112d4617 100644 --- a/test/e2e_test/regression/api_test.go +++ b/test/e2e_test/regression/api_test.go @@ -26,11 +26,14 @@ type EndToEndAPIRegressionTestSuite struct { func (s *EndToEndAPIRegressionTestSuite) SetupTest() { ctx := context.Background() + ctxOrgAdminAuth := metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) s.client, s.serviceDataClient, s.appConfig, s.cancelClient, s.cancelServiceDataClient, _ = testbench.SetupTests(s.T()) // validate // list user length is 10 because there are 8 mock data, 1 system email, and 1 admin email created in test setup - uRes, err := s.client.ListUsers(ctx, &shieldv1beta1.ListUsersRequest{}) + uRes, err := s.client.ListUsers(ctxOrgAdminAuth, &shieldv1beta1.ListUsersRequest{}) s.Require().NoError(err) s.Require().Equal(10, len(uRes.GetUsers())) @@ -40,15 +43,15 @@ func (s *EndToEndAPIRegressionTestSuite) SetupTest() { pRes, err := s.client.ListProjects(ctx, &shieldv1beta1.ListProjectsRequest{}) s.Require().NoError(err) - s.Require().Equal(1, len(pRes.GetProjects())) + s.Require().Equal(2, len(pRes.GetProjects())) - gRes, err := s.client.ListGroups(ctx, &shieldv1beta1.ListGroupsRequest{}) + gRes, err := s.client.ListGroups(ctxOrgAdminAuth, &shieldv1beta1.ListGroupsRequest{}) s.Require().NoError(err) s.Require().Equal(3, len(gRes.GetGroups())) rRes, err := s.client.ListResources(ctx, &shieldv1beta1.ListResourcesRequest{}) s.Require().NoError(err) - s.Require().Equal(2, len(rRes.GetResources())) + s.Require().Equal(5, len(rRes.GetResources())) } func (s *EndToEndAPIRegressionTestSuite) TearDownTest() { @@ -332,7 +335,7 @@ func (s *EndToEndAPIRegressionTestSuite) TestUserAPI() { Email: "new-user-a@gotocompany.com", Metadata: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "foo": structpb.NewBoolValue(true), + "test-key-01": structpb.NewBoolValue(true), }, }, }, @@ -346,7 +349,7 @@ func (s *EndToEndAPIRegressionTestSuite) TestUserAPI() { Email: "new-user-a@gotocompany.com", Metadata: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "foo": structpb.NewBoolValue(true), + "test-key-01": structpb.NewBoolValue(true), }, }, }, @@ -362,7 +365,7 @@ func (s *EndToEndAPIRegressionTestSuite) TestUserAPI() { Email: "admin1-group1-org1@gotocompany.com", Metadata: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "foo": structpb.NewBoolValue(true), + "test-key-01": structpb.NewBoolValue(true), }, }, }, @@ -381,7 +384,7 @@ func (s *EndToEndAPIRegressionTestSuite) TestUserAPI() { Email: "", Metadata: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "foo": structpb.NewBoolValue(true), + "test-key-01": structpb.NewBoolValue(true), }, }, }, @@ -396,7 +399,7 @@ func (s *EndToEndAPIRegressionTestSuite) TestUserAPI() { Email: "admin1-group1-org1@gotocompany.com", Metadata: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "foo": structpb.NewBoolValue(true), + "test-key-01": structpb.NewBoolValue(true), }, }, }, @@ -463,7 +466,7 @@ func (s *EndToEndAPIRegressionTestSuite) TestServiceDataAPI() { s.Require().Greater(len(res.GetProjects()), 0) myProject := res.GetProjects()[0] - usr, err := s.client.ListUsers(context.Background(), &shieldv1beta1.ListUsersRequest{}) + usr, err := s.client.ListUsers(ctxOrgAdminAuth, &shieldv1beta1.ListUsersRequest{}) s.Require().NoError(err) s.Require().Greater(len(usr.GetUsers()), 0) myUser := usr.GetUsers()[0] @@ -528,8 +531,10 @@ func (s *EndToEndAPIRegressionTestSuite) TestServiceDataAPI() { UserId: "invalid-user-id", Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: myProject.Id, - Data: map[string]string{ - "update-key": "update value", + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "update-key": structpb.NewStringValue("update value"), + }, }, }, }) @@ -541,8 +546,10 @@ func (s *EndToEndAPIRegressionTestSuite) TestServiceDataAPI() { UserId: testbench.OrgAdminEmail, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: "invalid-project-id", - Data: map[string]string{ - "update-key": "update value", + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "update-key": structpb.NewStringValue("update value"), + }, }, }, }) @@ -554,9 +561,11 @@ func (s *EndToEndAPIRegressionTestSuite) TestServiceDataAPI() { UserId: testbench.OrgAdminEmail, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: myProject.Id, - Data: map[string]string{ - "update-key-1": "update value-1", - "update-key-2": "update value-2", + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "update-key-1": structpb.NewStringValue("update value-1"), + "update-key-2": structpb.NewStringValue("update value-2"), + }, }, }, }) @@ -583,12 +592,14 @@ func (s *EndToEndAPIRegressionTestSuite) TestServiceDataAPI() { UserId: testbench.OrgAdminEmail, Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: myProject.Id, - Data: map[string]string{ - "new-key": "new-value", + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "new-key": structpb.NewStringValue("new-value"), + }, }, }, }) - s.Assert().Equal(codes.Unauthenticated, status.Convert(err).Code()) + s.Assert().Equal(codes.PermissionDenied, status.Convert(err).Code()) }) s.Run("9. org admin update a group service data with invalid group id should return invalid argument error", func() { @@ -596,8 +607,10 @@ func (s *EndToEndAPIRegressionTestSuite) TestServiceDataAPI() { GroupId: "invalid-group-id", Body: &shieldv1beta1.UpsertServiceDataRequestBody{ Project: myProject.Id, - Data: map[string]string{ - "update-key": "update value", + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "update-key": structpb.NewStringValue("update value"), + }, }, }, }) diff --git a/test/e2e_test/smoke/api_test.go b/test/e2e_test/smoke/api_test.go index c51e711f4..a0df9a38a 100644 --- a/test/e2e_test/smoke/api_test.go +++ b/test/e2e_test/smoke/api_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/goto/shield/config" + "github.com/goto/shield/internal/schema" shieldv1beta1 "github.com/goto/shield/proto/v1beta1" "github.com/goto/shield/test/e2e_test/testbench" "github.com/stretchr/testify/suite" @@ -24,6 +25,10 @@ type EndToEndAPISmokeTestSuite struct { func (s *EndToEndAPISmokeTestSuite) SetupTest() { ctx := context.Background() + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + s.client, _, s.appConfig, s.cancelClient, _, _ = testbench.SetupTests(s.T()) // validate @@ -39,11 +44,15 @@ func (s *EndToEndAPISmokeTestSuite) SetupTest() { pRes, err := s.client.ListProjects(ctx, &shieldv1beta1.ListProjectsRequest{}) s.Require().NoError(err) - s.Require().Equal(1, len(pRes.GetProjects())) + s.Require().Equal(2, len(pRes.GetProjects())) gRes, err := s.client.ListGroups(ctx, &shieldv1beta1.ListGroupsRequest{}) s.Require().NoError(err) s.Require().Equal(3, len(gRes.GetGroups())) + + rRes, err := s.client.ListResources(ctx, &shieldv1beta1.ListResourcesRequest{}) + s.Require().NoError(err) + s.Require().Equal(5, len(rRes.GetResources())) } func (s *EndToEndAPISmokeTestSuite) TearDownTest() { @@ -83,6 +92,42 @@ func (s *EndToEndAPISmokeTestSuite) TestUserAPI() { }) } +func (s *EndToEndAPISmokeTestSuite) TestRelationsAPI() { + ctxOrgAdminAuth := metadata.NewOutgoingContext(context.Background(), metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + + s.Run("1. should fail when trying to create wildcard relation", func() { + oRes, err := s.client.ListOrganizations(ctxOrgAdminAuth, &shieldv1beta1.ListOrganizationsRequest{}) + s.Require().NoError(err) + + _, err = s.client.CreateRelation(ctxOrgAdminAuth, &shieldv1beta1.CreateRelationRequest{ + Body: &shieldv1beta1.RelationRequestBody{ + ObjectId: oRes.Organizations[0].Id, + ObjectNamespace: schema.OrganizationNamespace, + Subject: schema.UserPrincipalWildcard, + RoleName: schema.OwnerRole, + }, + }) + s.Assert().Error(err) + }) + + s.Run("2. should allow relation creation with wildcard", func() { + res, err := s.client.ListResources(ctxOrgAdminAuth, &shieldv1beta1.ListResourcesRequest{}) + s.Require().NoError(err) + + _, err = s.client.CreateRelation(ctxOrgAdminAuth, &shieldv1beta1.CreateRelationRequest{ + Body: &shieldv1beta1.RelationRequestBody{ + ObjectId: res.Resources[0].Id, + ObjectNamespace: schema.ServiceDataKeyNamespace, + Subject: schema.UserPrincipalWildcard, + RoleName: schema.ViewerRole, + }, + }) + s.Assert().NoError(err) + }) +} + func TestEndToEndAPISmokeTestSuite(t *testing.T) { suite.Run(t, new(EndToEndAPISmokeTestSuite)) } diff --git a/test/e2e_test/smoke/proxy_test.go b/test/e2e_test/smoke/proxy_test.go index c8e14cc23..2c8d43a17 100644 --- a/test/e2e_test/smoke/proxy_test.go +++ b/test/e2e_test/smoke/proxy_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/suite" + "google.golang.org/grpc/metadata" "github.com/goto/shield/config" "github.com/goto/shield/pkg/db" @@ -35,6 +36,10 @@ type EndToEndProxySmokeTestSuite struct { func (s *EndToEndProxySmokeTestSuite) SetupTest() { ctx := context.Background() + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + s.client, s.serviceDataClient, s.appConfig, s.cancelClient, s.cancelServiceDataClient, _ = testbench.SetupTests(s.T()) dbClient, err := testbench.SetupDB(s.appConfig.DB) @@ -59,7 +64,7 @@ func (s *EndToEndProxySmokeTestSuite) SetupTest() { pRes, err := s.client.ListProjects(ctx, &shieldv1beta1.ListProjectsRequest{}) s.Require().NoError(err) - s.Require().Equal(1, len(pRes.GetProjects())) + s.Require().Equal(2, len(pRes.GetProjects())) s.projID = pRes.GetProjects()[0].GetId() s.projSlug = pRes.GetProjects()[0].GetSlug() @@ -141,7 +146,11 @@ func (s *EndToEndProxySmokeTestSuite) TestProxyToEchoServer() { }) s.Run("user not part of group will not be authenticated by middleware auth", func() { - groupDetail, err := s.client.GetGroup(context.Background(), &shieldv1beta1.GetGroupRequest{Id: s.groupID}) + ctx := context.Background() + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + groupDetail, err := s.client.GetGroup(ctx, &shieldv1beta1.GetGroupRequest{Id: s.groupID}) s.Require().NoError(err) url := fmt.Sprintf("http://localhost:%d/api/resource_slug", s.appConfig.Proxy.Services[0].Port) @@ -272,7 +281,11 @@ func (s *EndToEndProxySmokeTestSuite) TestProxyToEchoServer() { }) s.Run("resource created on echo server should persist in shieldDB when using group slug", func() { - groupDetail, err := s.client.GetGroup(context.Background(), &shieldv1beta1.GetGroupRequest{Id: s.groupID}) + ctx := context.Background() + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + groupDetail, err := s.client.GetGroup(ctx, &shieldv1beta1.GetGroupRequest{Id: s.groupID}) s.Require().NoError(err) url := fmt.Sprintf("http://localhost:%d/api/resource_slug", s.appConfig.Proxy.Services[0].Port) @@ -373,7 +386,11 @@ func (s *EndToEndProxySmokeTestSuite) TestProxyToEchoServer() { s.Assert().Equal(s.userID, subjectID) }) s.Run("resource created on echo server should persist in shieldDB when using user e-mail", func() { - userDetail, err := s.client.GetUser(context.Background(), &shieldv1beta1.GetUserRequest{Id: s.userID}) + ctx := context.Background() + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + userDetail, err := s.client.GetUser(ctx, &shieldv1beta1.GetUserRequest{Id: s.userID}) s.Require().NoError(err) url := fmt.Sprintf("http://localhost:%d/api/resource_user_email", s.appConfig.Proxy.Services[0].Port) @@ -425,7 +442,11 @@ func (s *EndToEndProxySmokeTestSuite) TestProxyToEchoServer() { s.Assert().Equal(s.userID, subjectID) }) s.Run("resource created on echo server should persist in shieldDB when using composite variable", func() { - userDetail, err := s.client.GetUser(context.Background(), &shieldv1beta1.GetUserRequest{Id: s.userID}) + ctx := context.Background() + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + userDetail, err := s.client.GetUser(ctx, &shieldv1beta1.GetUserRequest{Id: s.userID}) s.Require().NoError(err) url := fmt.Sprintf("http://localhost:%d/api/resource_composite/test-name", s.appConfig.Proxy.Services[0].Port) @@ -476,7 +497,11 @@ func (s *EndToEndProxySmokeTestSuite) TestProxyToEchoServer() { s.Assert().Equal(s.userID, subjectID) }) s.Run("permission expression: permission resource can be composed using multiple variable", func() { - userDetail, err := s.client.GetUser(context.Background(), &shieldv1beta1.GetUserRequest{Id: s.userID}) + ctx := context.Background() + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + testbench.IdentityHeader: testbench.OrgAdminEmail, + })) + userDetail, err := s.client.GetUser(ctx, &shieldv1beta1.GetUserRequest{Id: s.userID}) s.Require().NoError(err) url := fmt.Sprintf("http://localhost:%d/api/update_firehose_based_on_sink/test-name", s.appConfig.Proxy.Services[0].Port) diff --git a/test/e2e_test/testbench/helper.go b/test/e2e_test/testbench/helper.go index 7a3a11734..ca4a1f825 100644 --- a/test/e2e_test/testbench/helper.go +++ b/test/e2e_test/testbench/helper.go @@ -8,19 +8,22 @@ import ( "net" "os" + "github.com/goto/shield/core/servicedata" "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/db" shieldv1beta1 "github.com/goto/shield/proto/v1beta1" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/structpb" ) const ( - OrgAdminEmail = "admin1-group1-org1@gotocompany.com" - DefaultSystemEmail = "shield-service@gotocompany.com" - IdentityHeader = "X-Shield-Email" - userIDHeaderKey = "X-Shield-UserID" + OrgAdminEmail = "admin1-group1-org1@gotocompany.com" + DefaultSystemEmail = "shield-service@gotocompany.com" + IdentityHeader = "X-Shield-Email" + userIDHeaderKey = "X-Shield-UserID" + DefaultServiceDataProjectName = "system" ) func GetFreePort() (int, error) { @@ -124,6 +127,69 @@ func BootstrapMetadataKey(ctx context.Context, cl shieldv1beta1.ShieldServiceCli return nil } +func BootstrapServiceDataKey(ctx context.Context, cl shieldv1beta1.ServiceDataServiceClient, creatorEmail, defaultServiceDataProjectName, testDataPath string) error { + testFixtureJSON, err := os.ReadFile(testDataPath + "/mocks/mock-servicedata-keys.json") + if err != nil { + return err + } + + var data []servicedata.Key + if err = json.Unmarshal(testFixtureJSON, &data); err != nil { + return err + } + + for _, d := range data { + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + IdentityHeader: creatorEmail, + })) + _, err := cl.CreateServiceDataKey(ctx, &shieldv1beta1.CreateServiceDataKeyRequest{ + Body: &shieldv1beta1.ServiceDataKeyRequestBody{ + Project: DefaultServiceDataProjectName, + Key: d.Name, + Description: d.Description, + }, + }) + if err != nil { + return err + } + } + + return nil +} + +func BootstrapUserServiceData(ctx context.Context, cl shieldv1beta1.ServiceDataServiceClient, userID, creatorEmail, defaultServiceDataProjectName, testDataPath string) error { + testFixtureJSON, err := os.ReadFile(testDataPath + "/mocks/mock-servicedata.json") + if err != nil { + return err + } + var data []servicedata.ServiceData + if err = json.Unmarshal(testFixtureJSON, &data); err != nil { + return err + } + + for _, d := range data { + ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ + IdentityHeader: creatorEmail, + })) + _, err := cl.UpsertUserServiceData(ctx, &shieldv1beta1.UpsertUserServiceDataRequest{ + UserId: userID, + Body: &shieldv1beta1.UpsertServiceDataRequestBody{ + Project: DefaultServiceDataProjectName, + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + d.Key.Name: structpb.NewStringValue(d.Value.(string)), + }, + }, + }, + }) + if err != nil { + return err + } + } + + return nil +} + func BootstrapOrganization(ctx context.Context, cl shieldv1beta1.ShieldServiceClient, creatorEmail string, testDataPath string) error { testFixtureJSON, err := os.ReadFile(testDataPath + "/mocks/mock-organization.json") if err != nil { @@ -170,6 +236,7 @@ func BootstrapProject(ctx context.Context, cl shieldv1beta1.ShieldServiceClient, } data[0].OrgId = orgResp.GetOrganizations()[0].GetId() + data[1].OrgId = orgResp.GetOrganizations()[0].GetId() for _, d := range data { ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ diff --git a/test/e2e_test/testbench/spicedb.go b/test/e2e_test/testbench/spicedb.go index 16789ff2a..1751a9beb 100644 --- a/test/e2e_test/testbench/spicedb.go +++ b/test/e2e_test/testbench/spicedb.go @@ -14,9 +14,9 @@ import ( func migrateSpiceDB(logger log.Logger, network *docker.Network, pool *dockertest.Pool, pgConnString string) error { resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "quay.io/authzed/spicedb", - Tag: "v1.0.0", - Cmd: []string{"spicedb", "migrate", "head", "--datastore-engine", "postgres", "--datastore-conn-uri", pgConnString}, + Repository: "authzed/spicedb", + Tag: "v1.32.0", + Cmd: []string{"migrate", "head", "--datastore-engine", "postgres", "--datastore-conn-uri", pgConnString}, NetworkID: network.ID, }, func(config *docker.HostConfig) { config.RestartPolicy = docker.RestartPolicy{ @@ -67,9 +67,9 @@ func migrateSpiceDB(logger log.Logger, network *docker.Network, pool *dockertest func startSpiceDB(logger log.Logger, network *docker.Network, pool *dockertest.Pool, pgConnString string, preSharedKey string) (extPort string, res *dockertest.Resource, err error) { res, err = pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "quay.io/authzed/spicedb", - Tag: "v1.0.0", - Cmd: []string{"spicedb", "serve", "--log-level", "debug", "--grpc-preshared-key", preSharedKey, "--grpc-no-tls", "--datastore-engine", "postgres", "--datastore-conn-uri", pgConnString}, + Repository: "authzed/spicedb", + Tag: "v1.32.0", + Cmd: []string{"serve", "--log-level", "debug", "--grpc-preshared-key", preSharedKey, "--datastore-engine", "postgres", "--datastore-conn-uri", pgConnString}, ExposedPorts: []string{"50051/tcp"}, NetworkID: network.ID, }, func(config *docker.HostConfig) { diff --git a/test/e2e_test/testbench/testbench.go b/test/e2e_test/testbench/testbench.go index 84326bc1d..24f87a0f6 100644 --- a/test/e2e_test/testbench/testbench.go +++ b/test/e2e_test/testbench/testbench.go @@ -203,8 +203,9 @@ func SetupTests(t *testing.T) (shieldv1beta1.ShieldServiceClient, shieldv1beta1. ResourcesConfigPath: fmt.Sprintf("file://%s/%s", testDataPath, "configs/resources"), RulesPath: fmt.Sprintf("file://%s/%s", testDataPath, "configs/rules"), ServiceData: server.ServiceDataConfig{ - BootstrapEnabled: true, - MaxNumUpsertData: 1, + BootstrapEnabled: true, + MaxNumUpsertData: 1, + DefaultServiceDataProject: DefaultServiceDataProjectName, }, PublicAPIPrefix: "/shield", CacheConfig: inmemory.Config{ @@ -255,18 +256,18 @@ func SetupTests(t *testing.T) (shieldv1beta1.ShieldServiceClient, shieldv1beta1. }); err != nil { t.Fatal(err.Error()) } - - if err := BootstrapMetadataKey(ctx, client, OrgAdminEmail, testDataPath); err != nil { + if err := BootstrapOrganization(ctx, client, OrgAdminEmail, testDataPath); err != nil { t.Fatal(err) } - - if err := BootstrapUser(ctx, client, OrgAdminEmail, testDataPath); err != nil { + if err := BootstrapProject(ctx, client, OrgAdminEmail, testDataPath); err != nil { t.Fatal(err) } - if err := BootstrapOrganization(ctx, client, OrgAdminEmail, testDataPath); err != nil { + + if err := BootstrapServiceDataKey(ctx, serviceDataClient, OrgAdminEmail, DefaultServiceDataProjectName, testDataPath); err != nil { t.Fatal(err) } - if err := BootstrapProject(ctx, client, OrgAdminEmail, testDataPath); err != nil { + + if err := BootstrapUser(ctx, client, OrgAdminEmail, testDataPath); err != nil { t.Fatal(err) } if err := BootstrapGroup(ctx, client, OrgAdminEmail, testDataPath); err != nil { diff --git a/test/e2e_test/testbench/testdata/mocks/mock-project.json b/test/e2e_test/testbench/testdata/mocks/mock-project.json index e7c8e115e..8be4a1adf 100644 --- a/test/e2e_test/testbench/testdata/mocks/mock-project.json +++ b/test/e2e_test/testbench/testdata/mocks/mock-project.json @@ -10,5 +10,9 @@ "k2": "v2" } } + }, + { + "name": "system", + "slug": "system" } ] \ No newline at end of file diff --git a/test/e2e_test/testbench/testdata/mocks/mock-servicedata-keys.json b/test/e2e_test/testbench/testdata/mocks/mock-servicedata-keys.json new file mode 100644 index 000000000..b7cb14c02 --- /dev/null +++ b/test/e2e_test/testbench/testdata/mocks/mock-servicedata-keys.json @@ -0,0 +1,14 @@ +[ + { + "name": "test-key-01", + "description": "description for test-key-01" + }, + { + "name": "test-key-02", + "description": "description for test-key-02" + }, + { + "name": "test-key-03", + "description": "description for test-key-03" + } +] \ No newline at end of file diff --git a/test/e2e_test/testbench/testdata/mocks/mock-servicedata.json b/test/e2e_test/testbench/testdata/mocks/mock-servicedata.json new file mode 100644 index 000000000..116a5135d --- /dev/null +++ b/test/e2e_test/testbench/testdata/mocks/mock-servicedata.json @@ -0,0 +1,11 @@ +[ + { + "value": "test-value-01" + }, + { + "value": "test-value-02" + }, + { + "value": "test-value-03" + } +] \ No newline at end of file diff --git a/test/integration_test/rest_test.go b/test/integration_test/rest_test.go index cf2292c2c..73f828691 100644 --- a/test/integration_test/rest_test.go +++ b/test/integration_test/rest_test.go @@ -15,6 +15,7 @@ import ( "github.com/goto/shield/core/project" "github.com/goto/shield/core/relation" "github.com/goto/shield/core/resource" + "github.com/goto/shield/core/role" "github.com/goto/shield/core/rule" "github.com/goto/shield/core/user" "github.com/goto/shield/internal/adapter" @@ -309,7 +310,7 @@ func buildPipeline(logger log.Logger, proxy http.Handler, ruleService *rule.Serv func hookPipeline(log log.Logger) hook.Service { rootHook := hook.New() - relationAdapter := adapter.NewRelation(&group.Service{}, &user.Service{}, &relation.Service{}) + relationAdapter := adapter.NewRelation(&group.Service{}, &user.Service{}, &relation.Service{}, &role.Service{}) return authz_hook.New(log, rootHook, rootHook, &resource.Service{}, &relation.Service{}, relationAdapter, "X-Auth-Email") }