From dfd486cd4104bad33c56232e94763772464bc48c Mon Sep 17 00:00:00 2001 From: ryanfkeepers Date: Thu, 10 Aug 2023 14:57:04 -0600 Subject: [PATCH] distrubutes ServiceResource throughout path Adds utilization of the ServiceResource struct to all of the path package functionality. In general, this takes most instances where a service and resource are requested, and replaces those values with a slice of ServiceResource tuples. To keep the review smaller, this change does not update any packages outside of path. Therefore the tests and build are guaranteed to fail. The next PR, which updates all other packages with the changes, is necessary for both PRs to move forward. --- src/pkg/path/builder.go | 135 ++++++++++---------------- src/pkg/path/builder_test.go | 14 ++- src/pkg/path/category_type.go | 18 ---- src/pkg/path/drive_test.go | 5 +- src/pkg/path/path.go | 86 ++++++++-------- src/pkg/path/path_test.go | 96 +++++++++++------- src/pkg/path/resource_path.go | 51 +++++++--- src/pkg/path/resource_path_test.go | 53 +++++----- src/pkg/path/service_category_test.go | 116 +++++++--------------- src/pkg/path/service_resource.go | 60 ++++++++++++ src/pkg/path/service_resource_test.go | 79 ++++++++++++++- 11 files changed, 416 insertions(+), 297 deletions(-) diff --git a/src/pkg/path/builder.go b/src/pkg/path/builder.go index 1cf502079..2e0237a6c 100644 --- a/src/pkg/path/builder.go +++ b/src/pkg/path/builder.go @@ -203,28 +203,30 @@ func (pb Builder) withPrefix(elements ...string) *Builder { // verifyPrefix ensures that the tenant and resourceOwner are valid // values, and that the builder has some directory structure. -func (pb Builder) verifyPrefix(tenant, resourceOwner string) error { - if err := verifyInputValues(tenant, resourceOwner); err != nil { - return err - } +// func (pb Builder) verifyPrefix(tenant, resourceOwner string) error { +// if err := verifyPrefixValues(tenant, resourceOwner); err != nil { +// return err +// } - if len(pb.elements) == 0 { - return clues.New("missing path beyond prefix") - } +// if len(pb.elements) == 0 { +// return clues.New("missing path beyond prefix") +// } - return nil -} +// return nil +// } // --------------------------------------------------------------------------- // Data Layer Path Transformers // --------------------------------------------------------------------------- func (pb Builder) ToStreamStorePath( - tenant, purpose string, - service ServiceType, + tenant string, + srs []ServiceResource, isItem bool, ) (Path, error) { - if err := verifyInputValues(tenant, purpose); err != nil { + cat := DetailsCategory + + if err := verifyPrefixValues(tenant, srs, cat); err != nil { return nil, err } @@ -232,40 +234,18 @@ func (pb Builder) ToStreamStorePath( return nil, clues.New("missing path beyond prefix") } - metadataService := UnknownService + dlrp := newDataLayerResourcePath(pb, tenant, toMetadataServices(srs), cat, isItem) - switch service { - case ExchangeService: - metadataService = ExchangeMetadataService - case OneDriveService: - metadataService = OneDriveMetadataService - case SharePointService: - metadataService = SharePointMetadataService - } - - return &dataLayerResourcePath{ - Builder: *pb.withPrefix( - tenant, - metadataService.String(), - purpose, - DetailsCategory.String()), - service: metadataService, - category: DetailsCategory, - hasItem: isItem, - }, nil + return &dlrp, nil } func (pb Builder) ToServiceCategoryMetadataPath( - tenant, user string, - service ServiceType, - category CategoryType, + tenant string, + srs []ServiceResource, + cat CategoryType, isItem bool, ) (Path, error) { - if err := ValidateServiceAndCategory(service, category); err != nil { - return nil, err - } - - if err := verifyInputValues(tenant, user); err != nil { + if err := verifyPrefixValues(tenant, srs, cat); err != nil { return nil, err } @@ -273,79 +253,66 @@ func (pb Builder) ToServiceCategoryMetadataPath( return nil, clues.New("missing path beyond prefix") } - metadataService := UnknownService + dlrp := newDataLayerResourcePath(pb, tenant, toMetadataServices(srs), cat, isItem) - switch service { - case ExchangeService: - metadataService = ExchangeMetadataService - case OneDriveService: - metadataService = OneDriveMetadataService - case SharePointService: - metadataService = SharePointMetadataService - } - - return &dataLayerResourcePath{ - Builder: *pb.withPrefix( - tenant, - metadataService.String(), - user, - category.String(), - ), - service: metadataService, - category: category, - hasItem: isItem, - }, nil + return &dlrp, nil } func (pb Builder) ToDataLayerPath( - tenant, user string, - service ServiceType, - category CategoryType, + tenant string, + srs []ServiceResource, + cat CategoryType, isItem bool, ) (Path, error) { - if err := ValidateServiceAndCategory(service, category); err != nil { + if err := verifyPrefixValues(tenant, srs, cat); err != nil { return nil, err } - if err := pb.verifyPrefix(tenant, user); err != nil { - return nil, err - } + dlrp := newDataLayerResourcePath(pb, tenant, srs, cat, isItem) - return &dataLayerResourcePath{ - Builder: *pb.withPrefix( - tenant, - service.String(), - user, - category.String()), - service: service, - category: category, - hasItem: isItem, - }, nil + return &dlrp, nil } func (pb Builder) ToDataLayerExchangePathForCategory( - tenant, user string, + tenant, mailboxID string, category CategoryType, isItem bool, ) (Path, error) { - return pb.ToDataLayerPath(tenant, user, ExchangeService, category, isItem) + srs, err := NewServiceResources(ExchangeService, mailboxID) + if err != nil { + return nil, err + } + + return pb.ToDataLayerPath(tenant, srs, category, isItem) } func (pb Builder) ToDataLayerOneDrivePath( - tenant, user string, + tenant, userID string, isItem bool, ) (Path, error) { - return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem) + srs, err := NewServiceResources(OneDriveService, userID) + if err != nil { + return nil, err + } + + return pb.ToDataLayerPath(tenant, srs, FilesCategory, isItem) } func (pb Builder) ToDataLayerSharePointPath( - tenant, site string, + tenant, siteID string, category CategoryType, isItem bool, ) (Path, error) { - return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem) + srs, err := NewServiceResources(SharePointService, siteID) + if err != nil { + return nil, err + } + + return pb.ToDataLayerPath(tenant, srs, category, isItem) } +// TODO: ToDataLayerGroupsPath() + // --------------------------------------------------------------------------- // Stringers and PII Concealer Compliance // --------------------------------------------------------------------------- diff --git a/src/pkg/path/builder_test.go b/src/pkg/path/builder_test.go index cb483606d..a1833dbb1 100644 --- a/src/pkg/path/builder_test.go +++ b/src/pkg/path/builder_test.go @@ -46,7 +46,10 @@ func (suite *BuilderUnitSuite) TestAppend() { func (suite *BuilderUnitSuite) TestAppendItem() { t := suite.T() - p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar") + srs, err := NewServiceResources(ExchangeService, "ro") + require.NoError(t, err, clues.ToCore(err)) + + p, err := Build("t", srs, EmailCategory, false, "foo", "bar") require.NoError(t, err, clues.ToCore(err)) pb := p.ToBuilder() @@ -339,8 +342,13 @@ func (suite *BuilderUnitSuite) TestFolder() { } func (suite *BuilderUnitSuite) TestPIIHandling() { - p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item") - require.NoError(suite.T(), err) + t := suite.T() + + srs, err := NewServiceResources(ExchangeService, "ro") + require.NoError(t, err, clues.ToCore(err)) + + p, err := Build("t", srs, EventsCategory, true, "dir", "item") + require.NoError(t, err) table := []struct { name string diff --git a/src/pkg/path/category_type.go b/src/pkg/path/category_type.go index 4a992176f..328d63094 100644 --- a/src/pkg/path/category_type.go +++ b/src/pkg/path/category_type.go @@ -75,24 +75,6 @@ var serviceCategories = map[ServiceType]map[CategoryType]struct{}{ }, } -func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) { - service := toServiceType(s) - if service == UnknownService { - return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s)) - } - - category := ToCategoryType(c) - if category == UnknownCategory { - return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c)) - } - - if err := ValidateServiceAndCategory(service, category); err != nil { - return UnknownService, UnknownCategory, err - } - - return service, category, nil -} - func ValidateServiceAndCategory(service ServiceType, category CategoryType) error { cats, ok := serviceCategories[service] if !ok { diff --git a/src/pkg/path/drive_test.go b/src/pkg/path/drive_test.go index e457a4423..a4e149ce4 100644 --- a/src/pkg/path/drive_test.go +++ b/src/pkg/path/drive_test.go @@ -59,7 +59,10 @@ func (suite *OneDrivePathSuite) Test_ToOneDrivePath() { suite.Run(tt.name, func() { t := suite.T() - p, err := path.Build("tenant", "user", path.OneDriveService, path.FilesCategory, false, tt.pathElements...) + srs, err := path.NewServiceResources(path.OneDriveService, "user") + require.NoError(t, err, clues.ToCore(err)) + + p, err := path.Build("tenant", srs, path.FilesCategory, false, tt.pathElements...) require.NoError(suite.T(), err, clues.ToCore(err)) got, err := path.ToDrivePath(p) diff --git a/src/pkg/path/path.go b/src/pkg/path/path.go index 8f8463b42..8138f0f75 100644 --- a/src/pkg/path/path.go +++ b/src/pkg/path/path.go @@ -79,10 +79,12 @@ var ( // string. type Path interface { String() string - Service() ServiceType + // ServiceResources produces all of the services and subservices, along with + // the protected resource paired with the service, as contained in the path, + // in their order of appearance. + ServiceResources() []ServiceResource Category() CategoryType Tenant() string - ResourceOwner() string Folder(escaped bool) string Folders() Elements Item() string @@ -132,41 +134,29 @@ type RestorePaths struct { // --------------------------------------------------------------------------- func Build( - tenant, resourceOwner string, - service ServiceType, + tenant string, + srs []ServiceResource, category CategoryType, hasItem bool, elements ...string, ) (Path, error) { - b := Builder{}.Append(elements...) - - return b.ToDataLayerPath( - tenant, resourceOwner, - service, category, - hasItem) + return Builder{}. + Append(elements...). + ToDataLayerPath(tenant, srs, category, hasItem) } func BuildPrefix( - tenant, resourceOwner string, - s ServiceType, - c CategoryType, + tenant string, + srs []ServiceResource, + cat CategoryType, ) (Path, error) { - pb := Builder{} - - if err := ValidateServiceAndCategory(s, c); err != nil { + if err := verifyPrefixValues(tenant, srs, cat); err != nil { return nil, err } - if err := verifyInputValues(tenant, resourceOwner); err != nil { - return nil, err - } + dlrp := newDataLayerResourcePath(Builder{}, tenant, srs, cat, false) - return &dataLayerResourcePath{ - Builder: *pb.withPrefix(tenant, s.String(), resourceOwner, c.String()), - service: s, - category: c, - hasItem: false, - }, nil + return &dlrp, nil } // FromDataLayerPath parses the escaped path p, validates the elements in p @@ -186,24 +176,37 @@ func FromDataLayerPath(p string, isItem bool) (Path, error) { return nil, clues.Stack(errParsingPath, err).With("path_string", p) } + // initial check for minimum required elements: + // tenant, service, resource, category, container/item if len(pb.elements) < 5 { return nil, clues.New("path has too few segments").With("path_string", p) } - service, category, err := validateServiceAndCategoryStrings( - pb.elements[1], - pb.elements[3], - ) + srs, catIdx, err := ElementsToServiceResources(pb.elements[1:]) if err != nil { + return nil, clues.Stack(err) + } + + // follow-up check: if more than one service exists, revisit the len check. + if len(srs) > 1 && len(pb.elements) < 3+(2*len(srs)) { + return nil, clues.New("path has too few segments").With("path_string", p) + } + + // +1 to account for slicing the tenant when calling the transformer func. + category := ToCategoryType(pb.elements[catIdx+1]) + + if err := verifyPrefixValues(pb.elements[0], srs, category); err != nil { return nil, clues.Stack(errParsingPath, err).With("path_string", p) } - return &dataLayerResourcePath{ - Builder: *pb, - service: service, - category: category, - hasItem: isItem, - }, nil + dlrp := dataLayerResourcePath{ + Builder: *pb, + serviceResources: srs, + category: category, + hasItem: isItem, + } + + return &dlrp, nil } // TrimTrailingSlash takes an escaped path element and returns an escaped path @@ -290,16 +293,21 @@ func Split(segment string) []string { // Unexported Helpers // --------------------------------------------------------------------------- -func verifyInputValues(tenant, resourceOwner string) error { +func verifyPrefixValues( + tenant string, + srs []ServiceResource, + cat CategoryType, +) error { if len(tenant) == 0 { return clues.Stack(errMissingSegment, clues.New("tenant")) } - if len(resourceOwner) == 0 { - return clues.Stack(errMissingSegment, clues.New("resourceOwner")) + if err := validateServiceResources(srs); err != nil { + return err } - return nil + // only the final service is checked for its category validity + return ValidateServiceAndCategory(srs[len(srs)-1].Service, cat) } // escapeElement takes a single path element and escapes all characters that diff --git a/src/pkg/path/path_test.go b/src/pkg/path/path_test.go index ca3055983..faccdac0d 100644 --- a/src/pkg/path/path_test.go +++ b/src/pkg/path/path_test.go @@ -334,14 +334,12 @@ func (suite *PathUnitSuite) TestFromDataLayerPath() { testUser, testElement1, testElement2, - testElement3, - ), + testElement3), expectedFolder: fmt.Sprintf( "%s/%s/%s", testElementTrimmed, testElement2, - testElement3, - ), + testElement3), expectedSplit: []string{ testElementTrimmed, testElement2, @@ -351,8 +349,7 @@ func (suite *PathUnitSuite) TestFromDataLayerPath() { expectedItemFolder: fmt.Sprintf( "%s/%s", testElementTrimmed, - testElement2, - ), + testElement2), expectedItemSplit: []string{ testElementTrimmed, testElement2, @@ -366,14 +363,12 @@ func (suite *PathUnitSuite) TestFromDataLayerPath() { testUser, testElementTrimmed, testElement2, - testElement3, - ), + testElement3), expectedFolder: fmt.Sprintf( "%s/%s/%s", testElementTrimmed, testElement2, - testElement3, - ), + testElement3), expectedSplit: []string{ testElementTrimmed, testElement2, @@ -383,8 +378,7 @@ func (suite *PathUnitSuite) TestFromDataLayerPath() { expectedItemFolder: fmt.Sprintf( "%s/%s", testElementTrimmed, - testElement2, - ), + testElement2), expectedItemSplit: []string{ testElementTrimmed, testElement2, @@ -393,21 +387,27 @@ func (suite *PathUnitSuite) TestFromDataLayerPath() { } for service, cats := range serviceCategories { + for cat := range cats { + for _, item := range isItem { suite.Run(fmt.Sprintf("%s-%s-%s", service, cat, item.name), func() { + for _, test := range table { suite.Run(test.name, func() { - t := suite.T() - testPath := fmt.Sprintf(test.unescapedPath, service, cat) + var ( + t = suite.T() + testPath = fmt.Sprintf(test.unescapedPath, service, cat) + sr = ServiceResource{service, testUser} + ) p, err := FromDataLayerPath(testPath, item.isItem) require.NoError(t, err, clues.ToCore(err)) - assert.Equal(t, service, p.Service(), "service") + assert.Len(t, p.ServiceResources(), 1, "service resources") + assert.Equal(t, sr, p.ServiceResources()[0], "service resource") assert.Equal(t, cat, p.Category(), "category") assert.Equal(t, testTenant, p.Tenant(), "tenant") - assert.Equal(t, testUser, p.ResourceOwner(), "resource owner") fld := p.Folder(false) escfld := p.Folder(true) @@ -435,44 +435,74 @@ func (suite *PathUnitSuite) TestFromDataLayerPath() { func (suite *PathUnitSuite) TestBuildPrefix() { table := []struct { name string - service ServiceType - category CategoryType tenant string - owner string + srs []ServiceResource + category CategoryType expect string expectErr require.ErrorAssertionFunc }{ { name: "ok", - service: ExchangeService, - category: ContactsCategory, tenant: "t", - owner: "ro", - expect: join([]string{"t", ExchangeService.String(), "ro", ContactsCategory.String()}), + srs: []ServiceResource{{ExchangeService, "roo"}}, + category: ContactsCategory, + expect: join([]string{"t", ExchangeService.String(), "roo", ContactsCategory.String()}), + expectErr: require.NoError, + }, + { + name: "ok with subservice", + tenant: "t", + srs: []ServiceResource{ + {GroupsService, "roo"}, + {SharePointService, "oor"}, + }, + category: LibrariesCategory, + expect: join([]string{ + "t", + GroupsService.String(), "roo", + SharePointService.String(), "oor", + LibrariesCategory.String()}), expectErr: require.NoError, }, { name: "bad category", - service: ExchangeService, + srs: []ServiceResource{{ExchangeService, "roo"}}, category: FilesCategory, tenant: "t", - owner: "ro", expectErr: require.Error, }, { name: "bad tenant", - service: ExchangeService, - category: ContactsCategory, tenant: "", - owner: "ro", + srs: []ServiceResource{{ExchangeService, "roo"}}, + category: ContactsCategory, expectErr: require.Error, }, { - name: "bad owner", - service: ExchangeService, - category: ContactsCategory, + name: "bad resource", tenant: "t", - owner: "", + srs: []ServiceResource{{ExchangeService, ""}}, + category: ContactsCategory, + expectErr: require.Error, + }, + { + name: "bad subservice", + tenant: "t", + srs: []ServiceResource{ + {ExchangeService, "roo"}, + {OneDriveService, "oor"}, + }, + category: FilesCategory, + expectErr: require.Error, + }, + { + name: "bad subservice resource", + tenant: "t", + srs: []ServiceResource{ + {GroupsService, "roo"}, + {SharePointService, ""}, + }, + category: LibrariesCategory, expectErr: require.Error, }, } @@ -480,7 +510,7 @@ func (suite *PathUnitSuite) TestBuildPrefix() { suite.Run(test.name, func() { t := suite.T() - r, err := BuildPrefix(test.tenant, test.owner, test.service, test.category) + r, err := BuildPrefix(test.tenant, test.srs, test.category) test.expectErr(t, err, clues.ToCore(err)) if r == nil { diff --git a/src/pkg/path/resource_path.go b/src/pkg/path/resource_path.go index 10dd71eb7..37738131a 100644 --- a/src/pkg/path/resource_path.go +++ b/src/pkg/path/resource_path.go @@ -18,9 +18,28 @@ import ( // element after the prefix. type dataLayerResourcePath struct { Builder - category CategoryType - service ServiceType - hasItem bool + category CategoryType + serviceResources []ServiceResource + hasItem bool +} + +// performs no validation, assumes the caller has validated the inputs. +func newDataLayerResourcePath( + pb Builder, + tenant string, + srs []ServiceResource, + cat CategoryType, + isItem bool, +) dataLayerResourcePath { + pfx := append([]string{tenant}, ServiceResourcesToElements(srs)...) + pfx = append(pfx, cat.String()) + + return dataLayerResourcePath{ + Builder: *pb.withPrefix(pfx...), + serviceResources: srs, + category: cat, + hasItem: isItem, + } } // Tenant returns the tenant ID embedded in the dataLayerResourcePath. @@ -28,9 +47,8 @@ func (rp dataLayerResourcePath) Tenant() string { return rp.Builder.elements[0] } -// Service returns the ServiceType embedded in the dataLayerResourcePath. -func (rp dataLayerResourcePath) Service() ServiceType { - return rp.service +func (rp dataLayerResourcePath) ServiceResources() []ServiceResource { + return rp.serviceResources } // Category returns the CategoryType embedded in the dataLayerResourcePath. @@ -95,15 +113,18 @@ func (rp dataLayerResourcePath) Item() string { // Dir removes the last element from the path. If this would remove a // value that is part of the standard prefix structure, an error is returned. func (rp dataLayerResourcePath) Dir() (Path, error) { - if len(rp.elements) <= 4 { + // Dir is not allowed to slice off any prefix values. + // The prefix len is determined by the length of the number of + // service+resource tuples, plus 2 (tenant and category). + if len(rp.elements) <= 2+(2*len(rp.serviceResources)) { return nil, clues.New("unable to shorten path").With("path", rp) } return &dataLayerResourcePath{ - Builder: *rp.Builder.Dir(), - service: rp.service, - category: rp.category, - hasItem: false, + Builder: *rp.Builder.Dir(), + serviceResources: rp.serviceResources, + category: rp.category, + hasItem: false, }, nil } @@ -116,10 +137,10 @@ func (rp dataLayerResourcePath) Append( } return &dataLayerResourcePath{ - Builder: *rp.Builder.Append(elems...), - service: rp.service, - category: rp.category, - hasItem: isItem, + Builder: *rp.Builder.Append(elems...), + serviceResources: rp.serviceResources, + category: rp.category, + hasItem: isItem, }, nil } diff --git a/src/pkg/path/resource_path_test.go b/src/pkg/path/resource_path_test.go index e49f797e2..3a7dfd3c3 100644 --- a/src/pkg/path/resource_path_test.go +++ b/src/pkg/path/resource_path_test.go @@ -256,10 +256,10 @@ func (suite *DataLayerResourcePath) TestDir() { func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() { tenant := "a-tenant" - user := "a-user" + resource := "a-resource" table := []struct { name string - service path.ServiceType + srs []path.ServiceResource category path.CategoryType postfix []string expectedService path.ServiceType @@ -267,14 +267,14 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() { }{ { name: "NoPostfixPasses", - service: path.ExchangeService, + srs: []path.ServiceResource{{path.ExchangeService, resource}}, category: path.EmailCategory, expectedService: path.ExchangeMetadataService, check: assert.NoError, }, { name: "PostfixPasses", - service: path.ExchangeService, + srs: []path.ServiceResource{{path.ExchangeService, resource}}, category: path.EmailCategory, postfix: []string{"a", "b"}, expectedService: path.ExchangeMetadataService, @@ -282,48 +282,48 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() { }, { name: "Fails", - service: path.ExchangeService, + srs: []path.ServiceResource{{path.ExchangeService, resource}}, category: path.FilesCategory, check: assert.Error, }, { name: "Passes", - service: path.ExchangeService, + srs: []path.ServiceResource{{path.ExchangeService, resource}}, category: path.ContactsCategory, expectedService: path.ExchangeMetadataService, check: assert.NoError, }, { name: "Passes", - service: path.ExchangeService, + srs: []path.ServiceResource{{path.ExchangeService, resource}}, category: path.EventsCategory, expectedService: path.ExchangeMetadataService, check: assert.NoError, }, { name: "Passes", - service: path.OneDriveService, + srs: []path.ServiceResource{{path.OneDriveService, resource}}, category: path.FilesCategory, expectedService: path.OneDriveMetadataService, check: assert.NoError, }, { name: "Passes", - service: path.SharePointService, + srs: []path.ServiceResource{{path.SharePointService, resource}}, category: path.LibrariesCategory, expectedService: path.SharePointMetadataService, check: assert.NoError, }, { name: "Passes", - service: path.SharePointService, + srs: []path.ServiceResource{{path.SharePointService, resource}}, category: path.ListsCategory, expectedService: path.SharePointMetadataService, check: assert.NoError, }, { name: "Passes", - service: path.SharePointService, + srs: []path.ServiceResource{{path.SharePointService, resource}}, category: path.PagesCategory, expectedService: path.SharePointMetadataService, check: assert.NoError, @@ -331,27 +331,26 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() { } for _, test := range table { - suite.Run(strings.Join([]string{ + name := strings.Join([]string{ test.name, - test.service.String(), + test.srs[0].Service.String(), test.category.String(), - }, "_"), func() { + }, "_") + + suite.Run(name, func() { t := suite.T() pb := path.Builder{}.Append(test.postfix...) p, err := pb.ToServiceCategoryMetadataPath( tenant, - user, - test.service, + test.srs, test.category, false) test.check(t, err, clues.ToCore(err)) - if err != nil { - return + if err == nil { + assert.Equal(t, test.expectedService, p.ServiceResources()[0]) } - - assert.Equal(t, test.expectedService, p.Service()) }) } } @@ -402,9 +401,9 @@ func (suite *DataLayerResourcePath) TestToExchangePathForCategory() { } assert.Equal(t, testTenant, p.Tenant()) - assert.Equal(t, path.ExchangeService, p.Service()) + assert.Equal(t, path.ExchangeService, p.ServiceResources()[0].Service) assert.Equal(t, test.category, p.Category()) - assert.Equal(t, testUser, p.ResourceOwner()) + assert.Equal(t, testUser, p.ServiceResources()[0].ProtectedResource) assert.Equal(t, strings.Join(m.expectedFolders, "/"), p.Folder(false)) assert.Equal(t, path.Elements(m.expectedFolders), p.Folders()) assert.Equal(t, m.expectedItem, p.Item()) @@ -456,7 +455,10 @@ func (suite *PopulatedDataLayerResourcePath) TestService() { suite.Run(m.name, func() { t := suite.T() - assert.Equal(t, path.ExchangeService, suite.paths[m.isItem].Service()) + assert.Equal( + t, + path.ExchangeService, + suite.paths[m.isItem].ServiceResources()[0].Service) }) } } @@ -476,7 +478,10 @@ func (suite *PopulatedDataLayerResourcePath) TestResourceOwner() { suite.Run(m.name, func() { t := suite.T() - assert.Equal(t, testUser, suite.paths[m.isItem].ResourceOwner()) + assert.Equal( + t, + testUser, + suite.paths[m.isItem].ServiceResources()[0].ProtectedResource) }) } } diff --git a/src/pkg/path/service_category_test.go b/src/pkg/path/service_category_test.go index d2b19b244..37aa5774e 100644 --- a/src/pkg/path/service_category_test.go +++ b/src/pkg/path/service_category_test.go @@ -20,118 +20,76 @@ func TestServiceCategoryUnitSuite(t *testing.T) { suite.Run(t, s) } -func (suite *ServiceCategoryUnitSuite) TestValidateServiceAndCategoryBadStringErrors() { +func (suite *ServiceCategoryUnitSuite) TestVerifyPrefixValues() { table := []struct { name string - service string - category string - }{ - { - name: "Service", - service: "foo", - category: EmailCategory.String(), - }, - { - name: "Category", - service: ExchangeService.String(), - category: "foo", - }, - } - for _, test := range table { - suite.Run(test.name, func() { - _, _, err := validateServiceAndCategoryStrings(test.service, test.category) - assert.Error(suite.T(), err) - }) - } -} - -func (suite *ServiceCategoryUnitSuite) TestValidateServiceAndCategory() { - table := []struct { - name string - service string - category string - expectedService ServiceType - expectedCategory CategoryType - check assert.ErrorAssertionFunc + service ServiceType + category CategoryType + check assert.ErrorAssertionFunc }{ { name: "UnknownService", - service: UnknownService.String(), - category: EmailCategory.String(), + service: UnknownService, + category: EmailCategory, check: assert.Error, }, { name: "UnknownCategory", - service: ExchangeService.String(), - category: UnknownCategory.String(), + service: ExchangeService, + category: UnknownCategory, check: assert.Error, }, { - name: "BadServiceString", - service: "foo", - category: EmailCategory.String(), + name: "BadServiceType", + service: ServiceType(-1), + category: EmailCategory, check: assert.Error, }, { - name: "BadCategoryString", - service: ExchangeService.String(), - category: "foo", + name: "BadCategoryType", + service: ExchangeService, + category: CategoryType(-1), check: assert.Error, }, { - name: "ExchangeEmail", - service: ExchangeService.String(), - category: EmailCategory.String(), - expectedService: ExchangeService, - expectedCategory: EmailCategory, - check: assert.NoError, + name: "ExchangeEmail", + service: ExchangeService, + category: EmailCategory, + check: assert.NoError, }, { - name: "ExchangeContacts", - service: ExchangeService.String(), - category: ContactsCategory.String(), - expectedService: ExchangeService, - expectedCategory: ContactsCategory, - check: assert.NoError, + name: "ExchangeContacts", + service: ExchangeService, + category: ContactsCategory, + check: assert.NoError, }, { - name: "ExchangeEvents", - service: ExchangeService.String(), - category: EventsCategory.String(), - expectedService: ExchangeService, - expectedCategory: EventsCategory, - check: assert.NoError, + name: "ExchangeEvents", + service: ExchangeService, + category: EventsCategory, + check: assert.NoError, }, { - name: "OneDriveFiles", - service: OneDriveService.String(), - category: FilesCategory.String(), - expectedService: OneDriveService, - expectedCategory: FilesCategory, - check: assert.NoError, + name: "OneDriveFiles", + service: OneDriveService, + category: FilesCategory, + check: assert.NoError, }, { - name: "SharePointLibraries", - service: SharePointService.String(), - category: LibrariesCategory.String(), - expectedService: SharePointService, - expectedCategory: LibrariesCategory, - check: assert.NoError, + name: "SharePointLibraries", + service: SharePointService, + category: LibrariesCategory, + check: assert.NoError, }, } for _, test := range table { suite.Run(test.name, func() { t := suite.T() - s, c, err := validateServiceAndCategoryStrings(test.service, test.category) + srs := []ServiceResource{{test.service, "resource"}} + + err := verifyPrefixValues("tid", srs, test.category) test.check(t, err, clues.ToCore(err)) - - if err != nil { - return - } - - assert.Equal(t, test.expectedService, s) - assert.Equal(t, test.expectedCategory, c) }) } } diff --git a/src/pkg/path/service_resource.go b/src/pkg/path/service_resource.go index bbfc3f4c7..7b03fed82 100644 --- a/src/pkg/path/service_resource.go +++ b/src/pkg/path/service_resource.go @@ -85,6 +85,40 @@ func ServiceResourcesToElements(srs []ServiceResource) Elements { return es } +// ElementsToServiceResources turns as many pairs of elems as possible +// into ServiceResource tuples. Elems must begin with a service, but +// may contain more entries than there are service/resource pairs. +// This transformer will continue consuming elements until it finds an +// even-numbered index that cannot be cast to a ServiceType. +// Returns the serviceResource pairs, the first index containing element +// that is not part of a service/resource pair, and an error if elems is +// len==0 or contains no services. +func ElementsToServiceResources(elems Elements) ([]ServiceResource, int, error) { + if len(elems) == 0 { + return nil, -1, clues.Wrap(errMissingSegment, "service") + } + + var ( + srs = make([]ServiceResource, 0) + i int + ) + + for j := 1; i < len(elems); i, j = i+2, j+2 { + service := toServiceType(elems[i]) + if service == UnknownService { + if i == 0 { + return nil, -1, clues.Wrap(errMissingSegment, "service") + } + + break + } + + srs = append(srs, ServiceResource{service, elems[j]}) + } + + return srs, i, nil +} + // checks for the following: // 1. each ServiceResource is valid // 2. if len(srs) > 1, srs[i], srs[i+1] pass subservice checks. @@ -112,3 +146,29 @@ func validateServiceResources(srs []ServiceResource) error { return nil } + +// makes a copy of the slice with all of the Services swapped for their +// metadata service countterparts. +func toMetadataServices(srs []ServiceResource) []ServiceResource { + msrs := make([]ServiceResource, 0, len(srs)) + + for _, sr := range srs { + msr := sr + metadataService := UnknownService + + switch sr.Service { + case ExchangeService: + metadataService = ExchangeMetadataService + case OneDriveService: + metadataService = OneDriveMetadataService + case SharePointService: + metadataService = SharePointMetadataService + // TODO: add groups + } + + msr.Service = metadataService + msrs = append(msrs, msr) + } + + return msrs +} diff --git a/src/pkg/path/service_resource_test.go b/src/pkg/path/service_resource_test.go index 78dea7aab..29c9d964b 100644 --- a/src/pkg/path/service_resource_test.go +++ b/src/pkg/path/service_resource_test.go @@ -125,7 +125,7 @@ func (suite *ServiceResourceUnitSuite) TestValidateServiceResources() { } } -func (suite *ServiceResourceUnitSuite) TestServiceResourceElements() { +func (suite *ServiceResourceUnitSuite) TestServiceResourceToElements() { table := []struct { name string srs []ServiceResource @@ -164,3 +164,80 @@ func (suite *ServiceResourceUnitSuite) TestServiceResourceElements() { }) } } + +func (suite *ServiceResourceUnitSuite) TestElementsToServiceResource() { + table := []struct { + name string + elems Elements + expectErr assert.ErrorAssertionFunc + expectIdx int + expectSRS []ServiceResource + }{ + { + name: "empty", + elems: Elements{}, + expectErr: assert.Error, + expectIdx: -1, + expectSRS: nil, + }, + { + name: "nil", + elems: nil, + expectErr: assert.Error, + expectIdx: -1, + expectSRS: nil, + }, + { + name: "non-service 0th elem", + elems: Elements{"fnords"}, + expectErr: assert.Error, + expectIdx: -1, + expectSRS: nil, + }, + { + name: "non-service 2nd elem", + elems: Elements{ExchangeService.String(), "fnords", "smarf"}, + expectErr: assert.Error, + expectIdx: -1, + expectSRS: nil, + }, + { + name: "single serviceResource", + elems: Elements{ExchangeService.String(), "fnords"}, + expectErr: assert.NoError, + expectIdx: 2, + expectSRS: []ServiceResource{{ExchangeService, "fnords"}}, + }, + { + name: "single serviceResource and extra value", + elems: Elements{ExchangeService.String(), "fnords", "smarf"}, + expectErr: assert.NoError, + expectIdx: 2, + expectSRS: []ServiceResource{{ExchangeService, "fnords"}}, + }, + { + name: "multiple serviceResource", + elems: Elements{ExchangeService.String(), "fnords", OneDriveService.String(), "smarf"}, + expectErr: assert.NoError, + expectIdx: 4, + expectSRS: []ServiceResource{{ExchangeService, "fnords"}, {OneDriveService, "smarf"}}, + }, + { + name: "multiple serviceResource and extra value", + elems: Elements{ExchangeService.String(), "fnords", OneDriveService.String(), "smarf", "flaboigans"}, + expectErr: assert.NoError, + expectIdx: 4, + expectSRS: []ServiceResource{{ExchangeService, "fnords"}, {OneDriveService, "smarf"}}, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + srs, idx, err := ElementsToServiceResources(test.elems) + test.expectErr(t, err, clues.ToCore(err)) + assert.Equal(t, test.expectIdx, idx) + assert.Equal(t, test.expectSRS, srs) + }) + } +}