add extra catches for resource locked (#4499)

Adds two layers of extra catches for resoruceLocked errors.  First, in pkg/errs, adds error check funcs to ensure the source comparators are checked even if
they somehow got skipped in the lower layer packages.

Second, adds checks for resource locked throughout sites, users, and groups in the api layer.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-10-13 17:27:43 -06:00 committed by GitHub
parent 7419faab23
commit 1452e8c0c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 24 deletions

View File

@ -24,7 +24,7 @@ const (
// map of enums to errors. We might want to re-use an enum for multiple
// internal errors (ex: "ServiceNotEnabled" may exist in both graph and
// non-graph producers).
var internalToExternal = map[errEnum][]error{
var externalToInternal = map[errEnum][]error{
ApplicationThrottled: {graph.ErrApplicationThrottled},
BackupNotFound: {repository.ErrorBackupNotFound},
RepoAlreadyExists: {repository.ErrorRepoAlreadyExists},
@ -33,22 +33,43 @@ var internalToExternal = map[errEnum][]error{
ServiceNotEnabled: {graph.ErrServiceNotEnabled},
}
// Internal returns the internal errors which match to the public error category.
func Internal(enum errEnum) []error {
return internalToExternal[enum]
type ErrCheck func(error) bool
// map of enums to error comparators. The above map assumes that we
// always stack or wrap the sentinel error in the returned error. But in
// many places of error handling, we primarily rely on error comparison
// checks. This allows us to apply those comparison checks instead of relying
// only on sentinels.
var externalToInternalCheck = map[errEnum][]ErrCheck{
ApplicationThrottled: {graph.IsErrApplicationThrottled},
ResourceNotAccessible: {graph.IsErrResourceLocked},
ResourceOwnerNotFound: {graph.IsErrItemNotFound},
}
// Internal returns the internal errors and error checking functions which
// match to the public error enum.
func Internal(enum errEnum) ([]error, []ErrCheck) {
return externalToInternal[enum], externalToInternalCheck[enum]
}
// Is checks if the provided error contains an internal error that matches
// the public error category.
func Is(err error, enum errEnum) bool {
internalErrs, ok := internalToExternal[enum]
if !ok {
return false
internalErrs, ok := externalToInternal[enum]
if ok {
for _, target := range internalErrs {
if errors.Is(err, target) {
return true
}
}
}
for _, target := range internalErrs {
if errors.Is(err, target) {
return true
internalChecks, ok := externalToInternalCheck[enum]
if ok {
for _, check := range internalChecks {
if check(err) {
return true
}
}
}

View File

@ -20,20 +20,108 @@ func TestErrUnitSuite(t *testing.T) {
suite.Run(t, &ErrUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ErrUnitSuite) TestInternal() {
func (suite *ErrUnitSuite) TestInternal_errs() {
table := []struct {
get errEnum
expect []error
}{
{RepoAlreadyExists, []error{repository.ErrorRepoAlreadyExists}},
{BackupNotFound, []error{repository.ErrorBackupNotFound}},
{ServiceNotEnabled, []error{graph.ErrServiceNotEnabled}},
{ResourceOwnerNotFound, []error{graph.ErrResourceOwnerNotFound}},
{ResourceNotAccessible, []error{graph.ErrResourceLocked}},
{
get: ApplicationThrottled,
expect: []error{graph.ErrApplicationThrottled},
},
{
get: RepoAlreadyExists,
expect: []error{repository.ErrorRepoAlreadyExists},
},
{
get: BackupNotFound,
expect: []error{repository.ErrorBackupNotFound},
},
{
get: ServiceNotEnabled,
expect: []error{graph.ErrServiceNotEnabled},
},
{
get: ResourceOwnerNotFound,
expect: []error{graph.ErrResourceOwnerNotFound},
},
{
get: ResourceNotAccessible,
expect: []error{graph.ErrResourceLocked},
},
}
for _, test := range table {
suite.Run(string(test.get), func() {
assert.ElementsMatch(suite.T(), test.expect, Internal(test.get))
// can't compare func signatures
errs, _ := Internal(test.get)
assert.ElementsMatch(suite.T(), test.expect, errs)
})
}
}
func (suite *ErrUnitSuite) TestInternal_checks() {
table := []struct {
get errEnum
err error
expectHasChecks assert.ValueAssertionFunc
expect assert.BoolAssertionFunc
}{
{
get: ApplicationThrottled,
err: graph.ErrApplicationThrottled,
expectHasChecks: assert.NotEmpty,
expect: assert.True,
},
{
get: RepoAlreadyExists,
err: graph.ErrApplicationThrottled,
expectHasChecks: assert.Empty,
expect: assert.False,
},
{
get: BackupNotFound,
err: repository.ErrorBackupNotFound,
expectHasChecks: assert.Empty,
expect: assert.False,
},
{
get: ServiceNotEnabled,
err: graph.ErrServiceNotEnabled,
expectHasChecks: assert.Empty,
expect: assert.False,
},
{
get: ResourceOwnerNotFound,
// won't match, checks itemNotFound, which isn't an error enum
err: graph.ErrResourceOwnerNotFound,
expectHasChecks: assert.NotEmpty,
expect: assert.False,
},
{
get: ResourceNotAccessible,
err: graph.ErrResourceLocked,
expectHasChecks: assert.NotEmpty,
expect: assert.True,
},
}
for _, test := range table {
suite.Run(string(test.get), func() {
t := suite.T()
_, checks := Internal(test.get)
test.expectHasChecks(t, checks)
var result bool
for _, check := range checks {
if check(test.err) {
result = true
break
}
}
test.expect(t, result)
})
}
}
@ -43,11 +131,30 @@ func (suite *ErrUnitSuite) TestIs() {
target errEnum
err error
}{
{RepoAlreadyExists, repository.ErrorRepoAlreadyExists},
{BackupNotFound, repository.ErrorBackupNotFound},
{ServiceNotEnabled, graph.ErrServiceNotEnabled},
{ResourceOwnerNotFound, graph.ErrResourceOwnerNotFound},
{ResourceNotAccessible, graph.ErrResourceLocked},
{
target: ApplicationThrottled,
err: graph.ErrApplicationThrottled,
},
{
target: RepoAlreadyExists,
err: repository.ErrorRepoAlreadyExists,
},
{
target: BackupNotFound,
err: repository.ErrorBackupNotFound,
},
{
target: ServiceNotEnabled,
err: graph.ErrServiceNotEnabled,
},
{
target: ResourceOwnerNotFound,
err: graph.ErrResourceOwnerNotFound,
},
{
target: ResourceNotAccessible,
err: graph.ErrResourceLocked,
},
}
for _, test := range table {
suite.Run(string(test.target), func() {

View File

@ -128,6 +128,10 @@ func (c Groups) GetByID(
return group, nil
}
if graph.IsErrResourceLocked(err) {
return nil, graph.Stack(ctx, clues.Stack(graph.ErrResourceLocked, err))
}
logger.CtxErr(ctx, err).Info("finding group by id, falling back to display name")
}
@ -140,6 +144,10 @@ func (c Groups) GetByID(
resp, err := service.Client().Groups().Get(ctx, opts)
if err != nil {
if graph.IsErrResourceLocked(err) {
err = clues.Stack(graph.ErrResourceLocked, err)
}
return nil, graph.Wrap(ctx, err, "finding group by display name")
}

View File

@ -158,6 +158,10 @@ func (c Sites) GetByID(
err = clues.Stack(graph.ErrResourceOwnerNotFound, err)
}
if graph.IsErrResourceLocked(err) {
err = clues.Stack(graph.ErrResourceLocked, err)
}
return nil, err
}
@ -200,6 +204,10 @@ func (c Sites) GetByID(
err = clues.Stack(graph.ErrResourceOwnerNotFound, err)
}
if graph.IsErrResourceLocked(err) {
return nil, graph.Stack(ctx, clues.Stack(graph.ErrResourceLocked, err))
}
return nil, err
}

View File

@ -121,9 +121,12 @@ func (c Users) GetByID(ctx context.Context, identifier string) (models.Userable,
)
resp, err = c.Stable.Client().Users().ByUserId(identifier).Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting user")
if graph.IsErrResourceLocked(err) {
err = clues.Stack(graph.ErrResourceLocked, err)
}
return nil, graph.Stack(ctx, err)
}
return resp, err