Skip to content

Commit 2137db0

Browse files
authored
test: Handle Filter flake with ctx errors (#7119)
* test: Handle Fitler flake with ctx errors * Add unit test to check filter for proper error * Correctly return category of errors
1 parent c87ec48 commit 2137db0

File tree

3 files changed

+89
-12
lines changed

3 files changed

+89
-12
lines changed

coderd/rbac/authz.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subject Subject, a
137137
err := auth.Authorize(ctx, subject, action, o.RBACObject())
138138
if err == nil {
139139
filtered = append(filtered, o)
140+
} else if !IsUnauthorizedError(err) {
141+
// If the error is not the expected "Unauthorized" error, then
142+
// it is something unexpected.
143+
return nil, err
140144
}
141145
}
142146
return filtered, nil
@@ -155,6 +159,10 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subject Subject, a
155159
err := prepared.Authorize(ctx, rbacObj)
156160
if err == nil {
157161
filtered = append(filtered, object)
162+
} else if !IsUnauthorizedError(err) {
163+
// If the error is not the expected "Unauthorized" error, then
164+
// it is something unexpected.
165+
return nil, err
158166
}
159167
}
160168

@@ -319,7 +327,8 @@ func (a RegoAuthorizer) authorize(ctx context.Context, subject Subject, action A
319327

320328
results, err := a.query.Eval(ctx, rego.EvalParsedInput(astV))
321329
if err != nil {
322-
return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w", err), subject, action, object, results)
330+
err = correctCancelError(err)
331+
return xerrors.Errorf("evaluate rego: %w", err)
323332
}
324333

325334
if !results.Allowed() {
@@ -430,7 +439,8 @@ EachQueryLoop:
430439
// We need to eval each query with the newly known fields.
431440
results, err := q.Eval(ctx, rego.EvalParsedInput(parsed))
432441
if err != nil {
433-
continue EachQueryLoop
442+
err = correctCancelError(err)
443+
return xerrors.Errorf("eval error: %w", err)
434444
}
435445

436446
// If there are no results, then the query is false. This is because rego

coderd/rbac/authz_internal_test.go

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,68 @@ func (w fakeObject) RBACObject() Object {
3030
}
3131
}
3232

33+
// objectBomb is a wrapper around an Objecter that calls a function when
34+
// RBACObject is called.
35+
type objectBomb struct {
36+
Objecter
37+
bomb func()
38+
}
39+
40+
func (o *objectBomb) RBACObject() Object {
41+
o.bomb()
42+
return o.Objecter.RBACObject()
43+
}
44+
3345
func TestFilterError(t *testing.T) {
3446
t.Parallel()
35-
auth := NewAuthorizer(prometheus.NewRegistry())
36-
subject := Subject{
37-
ID: uuid.NewString(),
38-
Roles: RoleNames{},
39-
Groups: []string{},
40-
Scope: ScopeAll,
41-
}
47+
_ = objectBomb{}
4248

43-
_, err := Filter(context.Background(), auth, subject, ActionRead, []Object{ResourceUser, ResourceWorkspace})
44-
require.ErrorContains(t, err, "object types must be uniform")
49+
t.Run("DifferentResourceTypes", func(t *testing.T) {
50+
t.Parallel()
51+
52+
auth := NewAuthorizer(prometheus.NewRegistry())
53+
subject := Subject{
54+
ID: uuid.NewString(),
55+
Roles: RoleNames{},
56+
Groups: []string{},
57+
Scope: ScopeAll,
58+
}
59+
60+
_, err := Filter(context.Background(), auth, subject, ActionRead, []Object{ResourceUser, ResourceWorkspace})
61+
require.ErrorContains(t, err, "object types must be uniform")
62+
})
63+
64+
t.Run("CancelledContext", func(t *testing.T) {
65+
t.Parallel()
66+
t.Skipf("This test is racy as rego eval checks the ctx canceled in a go routine. " +
67+
"It is a coin flip if the query finishes before the 'cancel' is checked. " +
68+
"So sometimes the 'Authorize' call succeeds even if ctx is canceled.")
69+
70+
auth := NewAuthorizer(prometheus.NewRegistry())
71+
subject := Subject{
72+
ID: uuid.NewString(),
73+
Roles: RoleNames{
74+
RoleOwner(),
75+
},
76+
Groups: []string{},
77+
Scope: ScopeAll,
78+
}
79+
80+
ctx, cancel := context.WithCancel(context.Background())
81+
defer cancel()
82+
objects := []Objecter{
83+
ResourceUser,
84+
ResourceUser,
85+
&objectBomb{
86+
Objecter: ResourceUser,
87+
bomb: cancel,
88+
},
89+
ResourceUser,
90+
}
91+
92+
_, err := Filter(ctx, auth, subject, ActionRead, objects)
93+
require.ErrorIs(t, err, context.Canceled)
94+
})
4595
}
4696

4797
// TestFilter ensures the filter acts the same as an individual authorize.
@@ -170,7 +220,7 @@ func TestFilter(t *testing.T) {
170220
localObjects := make([]fakeObject, len(objects))
171221
copy(localObjects, objects)
172222

173-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
223+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
174224
defer cancel()
175225
auth := NewAuthorizer(prometheus.NewRegistry())
176226

coderd/rbac/error.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package rbac
22

33
import (
4+
"context"
45
"errors"
56
"flag"
67
"fmt"
78

89
"github.com/open-policy-agent/opa/rego"
10+
"github.com/open-policy-agent/opa/topdown"
11+
"golang.org/x/xerrors"
912
)
1013

1114
const (
@@ -97,3 +100,17 @@ func (*UnauthorizedError) As(target interface{}) bool {
97100
}
98101
return false
99102
}
103+
104+
// correctCancelError will return the correct error for a canceled context. This
105+
// is because rego changes a canceled context to a topdown.CancelErr. This error
106+
// is not helpful if the code is "canceled". To make the error conform with the
107+
// rest of our canceled errors, we will convert the error to a context.Canceled
108+
// error. No good information is lost, as the topdown.CancelErr provides the
109+
// location of the query that was canceled, which does not matter.
110+
func correctCancelError(err error) error {
111+
e := new(topdown.Error)
112+
if xerrors.As(err, &e) || e.Code == topdown.CancelErr {
113+
return context.Canceled
114+
}
115+
return err
116+
}

0 commit comments

Comments
 (0)