fullfill categorizer for exchange selectors (#602)

This commit is contained in:
Keepers 2022-08-19 11:43:53 -06:00 committed by GitHub
parent 4bf6e92098
commit daa939f5f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 271 additions and 76 deletions

View File

@ -179,7 +179,7 @@ func makeExchangeScope(granularity string, cat exchangeCategory, vs []string) Ex
func makeExchangeUserScope(user, granularity string, cat exchangeCategory, vs []string) ExchangeScope { func makeExchangeUserScope(user, granularity string, cat exchangeCategory, vs []string) ExchangeScope {
es := makeExchangeScope(granularity, cat, vs).set(ExchangeUser, user) es := makeExchangeScope(granularity, cat, vs).set(ExchangeUser, user)
es[scopeKeyResource] = user es[scopeKeyResource] = user
es[scopeKeyDataType] = cat.dataType() es[scopeKeyDataType] = cat.leafType().String()
return es return es
} }
@ -301,7 +301,7 @@ func makeExchangeFilterScope(cat, filterCat exchangeCategory, vs []string) Excha
scopeKeyCategory: cat.String(), scopeKeyCategory: cat.String(),
scopeKeyInfoFilter: filterCat.String(), scopeKeyInfoFilter: filterCat.String(),
scopeKeyResource: Filter, scopeKeyResource: Filter,
scopeKeyDataType: cat.dataType(), scopeKeyDataType: cat.leafType().String(),
filterCat.String(): join(vs...), filterCat.String(): join(vs...),
} }
} }
@ -383,17 +383,15 @@ func (d ExchangeDestination) Set(cat exchangeCategory, dest string) error {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Scopes // Categories
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type ( // exchangeCategory enumerates the type of the lowest level
// ExchangeScope specifies the data available // of data specified by the scope.
// when interfacing with the Exchange service. type exchangeCategory int
ExchangeScope scope
// exchangeCategory enumerates the type of the lowest level // interface compliance checks
// of data () in a scope. var _ categorizer = ExchangeCategoryUnknown
exchangeCategory int
)
//go:generate stringer -type=exchangeCategory //go:generate stringer -type=exchangeCategory
const ( const (
@ -441,35 +439,139 @@ func exchangeCatAtoI(s string) exchangeCategory {
} }
} }
// exchangeDataType returns the human-readable name of the core data type. // exchangePathSet describes the category type keys used in Exchange paths.
// Ex: ExchangeContactFolder.dataType() => ExchangeContact.String() // The order of each slice is important, and should match the order in which
// Ex: ExchangeEvent.dataType() => ExchangeEvent.String(). // these types appear in the canonical Path for each type.
func (ec exchangeCategory) dataType() string { var categoryPathSet = map[categorizer][]categorizer{
switch ec { ExchangeContact: {ExchangeUser, ExchangeContactFolder, ExchangeContact},
case ExchangeContact, ExchangeContactFolder: ExchangeEvent: {ExchangeUser, ExchangeEvent},
return ExchangeContact.String() ExchangeMail: {ExchangeUser, ExchangeMailFolder, ExchangeMail},
case ExchangeMail, ExchangeMailFolder: ExchangeUser: {ExchangeUser}, // the root category must be represented
return ExchangeMail.String()
}
return ec.String()
} }
// Granularity describes the granularity (directory || item) // leafType returns the leaf category of the receiver.
// of the data in scope // If the receiver category has multiple leaves (ex: User) or no leaves,
func (s ExchangeScope) Granularity() string { // (ex: Unknown), the receiver itself is returned.
return s[scopeKeyGranularity] // Ex: ExchangeContactFolder.leafType() => ExchangeContact
// Ex: ExchangeEvent.leafType() => ExchangeEvent
// Ex: ExchangeUser.leafType() => ExchangeUser
func (ec exchangeCategory) leafType() exchangeCategory {
switch ec {
case ExchangeContact, ExchangeContactFolder:
return ExchangeContact
case ExchangeMail, ExchangeMailFolder:
return ExchangeMail
}
return ec
} }
// isType checks if either the receiver is a supertype of the parameter,
// or if the parameter is a supertype of the receiver.
// if either value is an unknown types, the comparison is always false.
// if either value is the root type (user), the comparison is always true.
func (ec exchangeCategory) isType(cat exchangeCategory) bool {
if cat == ExchangeCategoryUnknown || ec == ExchangeCategoryUnknown {
return false
}
if cat == ExchangeUser || ec == ExchangeUser {
return true
}
return ec.leafType() == cat.leafType()
}
// includesType returns true if it matches the isType check for
// the receiver's service category.
func (ec exchangeCategory) includesType(cat categorizer) bool {
c, ok := cat.(exchangeCategory)
if !ok {
return false
}
return ec.isType(c)
}
// transforms a path to a map of identified properties.
// TODO: this should use service-specific funcs in the Paths pkg. Instead of
// peeking at the path directly, the caller should compare against values like
// path.UserID() and path.Folders().
//
// Malformed (ie, short len) paths will return incomplete results.
// Example:
// [tenantID, userID, "mail", mailFolder, mailID]
// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID}
func (ec exchangeCategory) pathValues(path []string) map[categorizer]string {
m := map[categorizer]string{}
if len(path) < 4 {
return m
}
m[ExchangeUser] = path[1]
/*
TODO/Notice:
Mail and Contacts contain folder structures, identified
in this code as being at index 3. This assumes a single
folder, while in reality users can express subfolder
hierarchies of arbirary depth. Subfolder handling is coming
at a later time.
*/
switch ec {
case ExchangeContact:
if len(path) < 5 {
return m
}
m[ExchangeContactFolder] = path[3]
m[ExchangeContact] = path[4]
case ExchangeEvent:
if len(path) < 4 {
return m
}
m[ExchangeEvent] = path[3]
case ExchangeMail:
if len(path) < 5 {
return m
}
m[ExchangeMailFolder] = path[3]
m[ExchangeMail] = path[4]
}
return m
}
// pathKeys returns a map of the path types, keyed by their leaf type,
func (ec exchangeCategory) pathKeys() []categorizer {
return categoryPathSet[ec.leafType()]
}
// ---------------------------------------------------------------------------
// Scopes
// ---------------------------------------------------------------------------
// ExchangeScope specifies the data available
// when interfacing with the Exchange service.
type ExchangeScope scope
// interface compliance checks
// var _ scoper = &ExchangeScope{}
// Category describes the type of the data in scope. // Category describes the type of the data in scope.
func (s ExchangeScope) Category() exchangeCategory { func (s ExchangeScope) Category() exchangeCategory {
return exchangeCatAtoI(s[scopeKeyCategory]) return exchangeCatAtoI(s[scopeKeyCategory])
} }
// categorizer type is a generic wrapper around Category.
// Primarily used by scopes.go to for abstract comparisons.
func (s ExchangeScope) categorizer() categorizer {
return s.Category()
}
// Filer describes the specific filter, and its target values. // Filer describes the specific filter, and its target values.
func (s ExchangeScope) Filter() exchangeCategory { func (s ExchangeScope) Filter() exchangeCategory {
return exchangeCatAtoI(s[scopeKeyInfoFilter]) return exchangeCatAtoI(s[scopeKeyInfoFilter])
} }
// Granularity describes the granularity (directory || item)
// of the data in scope
func (s ExchangeScope) Granularity() string {
return s[scopeKeyGranularity]
}
// IncludeCategory checks whether the scope includes a // IncludeCategory checks whether the scope includes a
// certain category of data. // certain category of data.
// Ex: to check if the scope includes mail data: // Ex: to check if the scope includes mail data:
@ -528,13 +630,6 @@ func (s ExchangeScope) set(cat exchangeCategory, v string) ExchangeScope {
return s return s
} }
var categoryPathSet = map[exchangeCategory][]exchangeCategory{
ExchangeContact: {ExchangeUser, ExchangeContactFolder, ExchangeContact},
ExchangeEvent: {ExchangeUser, ExchangeEvent},
ExchangeMail: {ExchangeUser, ExchangeMailFolder, ExchangeMail},
ExchangeUser: {ExchangeUser},
}
// matches returns true if either the path or the info matches the scope details. // matches returns true if either the path or the info matches the scope details.
func (s ExchangeScope) matches(cat exchangeCategory, path []string, info *details.ExchangeInfo) bool { func (s ExchangeScope) matches(cat exchangeCategory, path []string, info *details.ExchangeInfo) bool {
return s.matchesPath(cat, path) || s.matchesInfo(cat, info) return s.matchesPath(cat, path) || s.matchesInfo(cat, info)
@ -586,9 +681,9 @@ func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeI
// matchesPath handles the standard behavior when comparing a scope and a path // matchesPath handles the standard behavior when comparing a scope and a path
// returns true if the scope and path match for the provided category. // returns true if the scope and path match for the provided category.
func (s ExchangeScope) matchesPath(cat exchangeCategory, path []string) bool { func (s ExchangeScope) matchesPath(cat exchangeCategory, path []string) bool {
pathIDs := exchangeIDPath(cat, path) pathIDs := cat.pathValues(path)
for _, c := range categoryPathSet[cat] { for _, c := range categoryPathSet[cat] {
target := s.Get(c) target := s.Get(c.(exchangeCategory))
// the scope must define the targets to match on // the scope must define the targets to match on
if len(target) == 0 { if len(target) == 0 {
return false return false
@ -617,47 +712,6 @@ func (s ExchangeScope) matchesPath(cat exchangeCategory, path []string) bool {
// Restore Point Filtering // Restore Point Filtering
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// transforms a path to a map of identified properties.
// Malformed (ie, short len) paths will return incomplete results.
// Example:
// [tenantID, userID, "mail", mailFolder, mailID]
// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID}
func exchangeIDPath(cat exchangeCategory, path []string) map[exchangeCategory]string {
m := map[exchangeCategory]string{}
if len(path) == 0 {
return m
}
m[ExchangeUser] = path[1]
/*
TODO/Notice:
Mail and Contacts contain folder structures, identified
in this code as being at index 3. This assumes a single
folder, while in reality users can express subfolder
hierarchies of arbirary depth. Subfolder handling is coming
at a later time.
*/
switch cat {
case ExchangeContact:
if len(path) < 5 {
return m
}
m[ExchangeContactFolder] = path[3]
m[ExchangeContact] = path[4]
case ExchangeEvent:
if len(path) < 4 {
return m
}
m[ExchangeEvent] = path[3]
case ExchangeMail:
if len(path) < 5 {
return m
}
m[ExchangeMailFolder] = path[3]
m[ExchangeMail] = path[4]
}
return m
}
// Reduce reduces the entries in a backupDetails struct to only // Reduce reduces the entries in a backupDetails struct to only
// those that match the inclusions, filters, and exclusions in the selector. // those that match the inclusions, filters, and exclusions in the selector.
func (sr *ExchangeRestore) Reduce(deets *details.Details) *details.Details { func (sr *ExchangeRestore) Reduce(deets *details.Details) *details.Details {

View File

@ -942,3 +942,144 @@ func (suite *ExchangeSourceSuite) TestIsAny() {
}) })
} }
} }
func (suite *ExchangeSourceSuite) TestExchangeCategory_LeafType() {
table := []struct {
cat exchangeCategory
expect exchangeCategory
}{
{exchangeCategory(-1), exchangeCategory(-1)},
{ExchangeCategoryUnknown, ExchangeCategoryUnknown},
{ExchangeUser, ExchangeUser},
{ExchangeMailFolder, ExchangeMail},
{ExchangeMail, ExchangeMail},
{ExchangeContactFolder, ExchangeContact},
{ExchangeEvent, ExchangeEvent},
}
for _, test := range table {
suite.T().Run(test.cat.String(), func(t *testing.T) {
assert.Equal(t, test.expect, test.cat.leafType(), test.cat.String())
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeCategory_IsType() {
table := []struct {
cat exchangeCategory
input exchangeCategory
expect assert.BoolAssertionFunc
}{
// technically this should be false, but we're not reducing fabricated
// exchange category values to unknown. Since the type is unexported,
// that transformation would be unnecessary.
{exchangeCategory(-1), exchangeCategory(-1), assert.True},
{ExchangeCategoryUnknown, ExchangeCategoryUnknown, assert.False},
{ExchangeUser, ExchangeCategoryUnknown, assert.False},
{ExchangeCategoryUnknown, ExchangeUser, assert.False},
{ExchangeUser, ExchangeUser, assert.True},
{ExchangeMailFolder, ExchangeMail, assert.True},
{ExchangeMailFolder, ExchangeContact, assert.False},
{ExchangeContactFolder, ExchangeMail, assert.False},
{ExchangeMail, ExchangeMail, assert.True},
{ExchangeContactFolder, ExchangeContact, assert.True},
{ExchangeContactFolder, ExchangeEvent, assert.False},
{ExchangeEvent, ExchangeContact, assert.False},
{ExchangeEvent, ExchangeEvent, assert.True},
}
for _, test := range table {
suite.T().Run(test.cat.String(), func(t *testing.T) {
test.expect(t, test.cat.isType(test.input))
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeCategory_IncludesType() {
table := []struct {
cat categorizer
input categorizer
expect assert.BoolAssertionFunc
}{
// technically this should be false, but we're not reducing fabricated
// exchange category values to unknown. Since the type is unexported,
// that transformation would be unnecessary.
{exchangeCategory(-1), exchangeCategory(-1), assert.True},
{ExchangeCategoryUnknown, ExchangeCategoryUnknown, assert.False},
{ExchangeUser, ExchangeCategoryUnknown, assert.False},
{ExchangeCategoryUnknown, ExchangeUser, assert.False},
{ExchangeUser, ExchangeUser, assert.True},
{ExchangeMailFolder, ExchangeMail, assert.True},
{ExchangeMailFolder, ExchangeContact, assert.False},
{ExchangeContactFolder, ExchangeMail, assert.False},
{ExchangeMail, ExchangeMail, assert.True},
{ExchangeContactFolder, ExchangeContact, assert.True},
{ExchangeContactFolder, ExchangeEvent, assert.False},
{ExchangeEvent, ExchangeContact, assert.False},
{ExchangeEvent, ExchangeEvent, assert.True},
{ExchangeUser, rootCatStub, assert.False},
{rootCatStub, ExchangeUser, assert.False},
}
for _, test := range table {
suite.T().Run(test.cat.String(), func(t *testing.T) {
test.expect(t, test.cat.includesType(test.input))
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeCategory_PathValues() {
contactPath := []string{"ten", "user", "contact", "cfolder", "contactitem"}
contactMap := map[categorizer]string{
ExchangeUser: contactPath[1],
ExchangeContactFolder: contactPath[3],
ExchangeContact: contactPath[4],
}
eventPath := []string{"ten", "user", "event", "eventitem"}
eventMap := map[categorizer]string{
ExchangeUser: eventPath[1],
ExchangeEvent: eventPath[3],
}
mailPath := []string{"ten", "user", "mail", "mfolder", "mailitem"}
mailMap := map[categorizer]string{
ExchangeUser: contactPath[1],
ExchangeMailFolder: mailPath[3],
ExchangeMail: mailPath[4],
}
table := []struct {
cat exchangeCategory
path []string
expect map[categorizer]string
}{
{ExchangeCategoryUnknown, nil, map[categorizer]string{}},
{ExchangeCategoryUnknown, []string{"a", "b", "c"}, map[categorizer]string{}},
{ExchangeContact, contactPath, contactMap},
{ExchangeEvent, eventPath, eventMap},
{ExchangeMail, mailPath, mailMap},
}
for _, test := range table {
suite.T().Run(test.cat.String(), func(t *testing.T) {
assert.Equal(t, test.cat.pathValues(test.path), test.expect)
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeCategory_PathKeys() {
contact := []categorizer{ExchangeUser, ExchangeContactFolder, ExchangeContact}
event := []categorizer{ExchangeUser, ExchangeEvent}
mail := []categorizer{ExchangeUser, ExchangeMailFolder, ExchangeMail}
user := []categorizer{ExchangeUser}
var empty []categorizer
table := []struct {
cat exchangeCategory
expect []categorizer
}{
{ExchangeCategoryUnknown, empty},
{ExchangeContact, contact},
{ExchangeEvent, event},
{ExchangeMail, mail},
{ExchangeUser, user},
}
for _, test := range table {
suite.T().Run(test.cat.String(), func(t *testing.T) {
assert.Equal(t, test.cat.pathKeys(), test.expect)
})
}
}