Skip to content

Commit ff73510

Browse files
committed
Push some new test and recoding logic
1 parent 5accbfe commit ff73510

File tree

7 files changed

+272
-1
lines changed

7 files changed

+272
-1
lines changed

coderd/authzquery/authz_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import (
44
"context"
55
"reflect"
66
"testing"
7+
"time"
8+
9+
"github.com/moby/moby/pkg/namesgenerator"
10+
11+
"github.com/coder/coder/testutil"
12+
13+
"github.com/coder/coder/coderd/database"
14+
"github.com/stretchr/testify/require"
715

816
"github.com/google/uuid"
917

@@ -41,3 +49,128 @@ func TestAuthzQueryRecursive(t *testing.T) {
4149
reflect.ValueOf(q).Method(i).Call(ins)
4250
}
4351
}
52+
53+
type authorizeTest struct {
54+
Data func(t *testing.T, tc *authorizeTest) map[string]interface{}
55+
// Test is all the calls to the AuthzStore
56+
Test func(ctx context.Context, t *testing.T, tc *authorizeTest, q authzquery.AuthzStore)
57+
// Assert is the objects and the expected RBAC calls.
58+
// If 2 reads are expected on the same object, pass in 2 rbac.Reads.
59+
Asserts map[string][]rbac.Action
60+
61+
names map[string]uuid.UUID
62+
}
63+
64+
func (tc *authorizeTest) Lookup(name string) uuid.UUID {
65+
if tc.names == nil {
66+
tc.names = make(map[string]uuid.UUID)
67+
}
68+
if id, ok := tc.names[name]; ok {
69+
return id
70+
}
71+
id := uuid.New()
72+
tc.names[name] = id
73+
return id
74+
}
75+
76+
func testAuthorizeFunction(t *testing.T, testCase *authorizeTest) {
77+
t.Helper()
78+
79+
// The actor does not really matter since all authz calls will succeed.
80+
actor := rbac.Subject{
81+
ID: uuid.New().String(),
82+
Roles: rbac.RoleNames{},
83+
Groups: []string{},
84+
Scope: rbac.ScopeAll,
85+
}
86+
87+
// Always use a fake database.
88+
db := databasefake.New()
89+
90+
// Record all authorization calls. This will allow all authorization calls
91+
// to succeed.
92+
rec := &coderdtest.RecordingAuthorizer{}
93+
q := authzquery.NewAuthzQuerier(db, rec)
94+
95+
// Setup Context
96+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
97+
ctx = authzquery.WithAuthorizeContext(ctx, actor)
98+
t.Cleanup(cancel)
99+
100+
// Seed all data into the database that is required for the test.
101+
data := setupTestData(t, testCase, db, ctx)
102+
103+
// Run the test.
104+
testCase.Test(ctx, t, testCase, q)
105+
106+
// Asset RBAC calls.
107+
pairs := make([]coderdtest.ActionObjectPair, 0)
108+
for objectName, asserts := range testCase.Asserts {
109+
object := data[objectName]
110+
for _, assert := range asserts {
111+
pairs = append(pairs, rec.Pair(assert, object))
112+
}
113+
}
114+
rec.UnorderedAssertActor(t, actor, pairs...)
115+
require.NoError(t, rec.AllAsserted(), "all authz checks asserted")
116+
}
117+
118+
func setupTestData(t *testing.T, testCase *authorizeTest, db database.Store, ctx context.Context) map[string]rbac.Objecter {
119+
rbacObjects := make(map[string]rbac.Objecter)
120+
// Setup the test data.
121+
orgID := uuid.New()
122+
data := testCase.Data(t, testCase)
123+
for name, v := range data {
124+
switch orig := v.(type) {
125+
case database.Template:
126+
template, err := db.InsertTemplate(ctx, database.InsertTemplateParams{
127+
ID: testCase.Lookup(name),
128+
CreatedAt: time.Now(),
129+
UpdatedAt: time.Now(),
130+
OrganizationID: takeFirst(orig.OrganizationID, orgID),
131+
Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)),
132+
Provisioner: takeFirst(orig.Provisioner, database.ProvisionerTypeEcho),
133+
ActiveVersionID: takeFirst(orig.ActiveVersionID, uuid.New()),
134+
Description: takeFirst(orig.Description, namesgenerator.GetRandomName(1)),
135+
DefaultTTL: takeFirst(orig.DefaultTTL, 3600),
136+
CreatedBy: takeFirst(orig.CreatedBy, uuid.New()),
137+
Icon: takeFirst(orig.Icon, namesgenerator.GetRandomName(1)),
138+
UserACL: orig.UserACL,
139+
GroupACL: orig.GroupACL,
140+
DisplayName: takeFirst(orig.DisplayName, namesgenerator.GetRandomName(1)),
141+
AllowUserCancelWorkspaceJobs: takeFirst(orig.AllowUserCancelWorkspaceJobs, true),
142+
})
143+
require.NoError(t, err, "insert template")
144+
145+
// Reinsert the template.
146+
data[name] = template
147+
rbacObjects[name] = template
148+
case database.Workspace:
149+
workspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
150+
ID: testCase.Lookup(name),
151+
CreatedAt: time.Now(),
152+
UpdatedAt: time.Now(),
153+
OrganizationID: takeFirst(orig.OrganizationID, orgID),
154+
TemplateID: takeFirst(orig.TemplateID, uuid.New()),
155+
Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)),
156+
AutostartSchedule: orig.AutostartSchedule,
157+
Ttl: orig.Ttl,
158+
})
159+
require.NoError(t, err, "insert workspace")
160+
161+
// Reinsert the workspace.
162+
data[name] = workspace
163+
rbacObjects[name] = workspace
164+
}
165+
}
166+
return rbacObjects
167+
}
168+
169+
// takeFirst will take the first non empty value.
170+
func takeFirst[Value comparable](def Value, next Value) Value {
171+
var empty Value
172+
if def == empty {
173+
return next
174+
}
175+
return def
176+
}

coderd/authzquery/authzquerier.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ var _ database.Store = (*AuthzQuerier)(nil)
2323
type AuthzQuerier struct {
2424
database database.Store
2525
authorizer rbac.Authorizer
26+
27+
// constantActor makes all actors on context ignored.
28+
constantActor *rbac.Subject
2629
}
2730

2831
func NewAuthzQuerier(db database.Store, authorizer rbac.Authorizer) *AuthzQuerier {
@@ -50,6 +53,10 @@ func (q *AuthzQuerier) InTx(function func(querier database.Store) error, txOpts
5053
}, txOpts)
5154
}
5255

56+
func (q *AuthzQuerier) As(subject rbac.Subject) database.Store {
57+
return NewAuthzQuerier(q.database, q.authorizer, subject)
58+
}
59+
5360
// authorizeContext is a helper function to authorize an action on an object.
5461
func (q *AuthzQuerier) authorizeContext(ctx context.Context, action rbac.Action, object rbac.Objecter) error {
5562
act, ok := actorFromContext(ctx)

coderd/authzquery/workspace_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,56 @@ import (
1919
"github.com/coder/coder/coderd/database/databasefake"
2020
)
2121

22+
func TestWorkspaceFunctions(t *testing.T) {
23+
t.Parallel()
24+
25+
testCases := []struct {
26+
Name string
27+
Config *authorizeTest
28+
}{
29+
{
30+
Name: "GetByID",
31+
Config: &authorizeTest{
32+
Data: func(t *testing.T, tc *authorizeTest) map[string]interface{} {
33+
return map[string]interface{}{
34+
"u-one": database.User{},
35+
"w-one": database.Workspace{
36+
Name: "peter-pan",
37+
OwnerID: tc.Lookup("u-one"),
38+
TemplateID: tc.Lookup("t-one"),
39+
},
40+
"t-one": database.Template{},
41+
}
42+
},
43+
Test: func(ctx context.Context, t *testing.T, tc *authorizeTest, q authzquery.AuthzStore) {
44+
wrk, err := q.GetWorkspaceByID(ctx, tc.Lookup("w-one"))
45+
require.NoError(t, err)
46+
47+
wrk, err = q.GetWorkspaceByID(ctx, tc.Lookup("w-one"))
48+
require.NoError(t, err)
49+
50+
_, err = q.GetTemplateByID(ctx, wrk.TemplateID)
51+
require.NoError(t, err)
52+
},
53+
Asserts: map[string][]rbac.Action{
54+
"w-one": {rbac.ActionRead, rbac.ActionRead},
55+
"t-one": {rbac.ActionRead},
56+
},
57+
},
58+
},
59+
}
60+
61+
for _, tc := range testCases {
62+
tc := tc
63+
t.Run(tc.Name, func(t *testing.T) {
64+
t.Parallel()
65+
66+
testAuthorizeFunction(t, tc.Config)
67+
})
68+
}
69+
70+
}
71+
2272
func TestWorkspace(t *testing.T) {
2373
// GetWorkspaceByID
2474
var (

coderd/coderdtest/authorize.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,35 @@ func (r *RecordingAuthorizer) AllAsserted() error {
576576
return nil
577577
}
578578

579-
// AssertActor asserts in order.
579+
// UnorderedAssertActor is the same as AssertActor, except it doesn't care about
580+
// order. It will assert the first call that matches the actor and pair.
581+
// It will not assert the same call twice, so if there is a duplicate assertion,
582+
// the pair will need to be passed in twice.
583+
func (r *RecordingAuthorizer) UnorderedAssertActor(t *testing.T, actor rbac.Subject, dids ...ActionObjectPair) {
584+
for _, did := range dids {
585+
found := false
586+
InnerCalledLoop:
587+
for i, c := range r.Called {
588+
if c.asserted {
589+
// Do not assert an already asserted call.
590+
continue
591+
}
592+
593+
if c.Action == did.Action &&
594+
c.Object.Equal(did.Object) &&
595+
c.Actor.Equal(actor) {
596+
597+
r.Called[i].asserted = true
598+
found = true
599+
break InnerCalledLoop
600+
}
601+
}
602+
require.Truef(t, found, "did not find call for %s %s", did.Action, did.Object.Type)
603+
}
604+
}
605+
606+
// AssertActor asserts in order. If the order of authz calls does not match,
607+
// this will fail.
580608
func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
581609
ptr := 0
582610
for i, call := range r.Called {

coderd/rbac/authz.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"sync"
77
"time"
88

9+
"github.com/coder/coder/coderd/util/slice"
10+
911
"github.com/open-policy-agent/opa/rego"
1012
"github.com/prometheus/client_golang/prometheus"
1113
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -26,6 +28,25 @@ type Subject struct {
2628
Scope ExpandableScope
2729
}
2830

31+
func (s Subject) Equal(b Subject) bool {
32+
if s.ID != b.ID {
33+
return false
34+
}
35+
36+
if !slice.SameElements(s.Groups, b.Groups) {
37+
return false
38+
}
39+
40+
if !slice.SameElements(s.SafeRoleNames(), b.SafeRoleNames()) {
41+
return false
42+
}
43+
44+
if s.SafeScopeName() != b.SafeScopeName() {
45+
return false
46+
}
47+
return true
48+
}
49+
2950
// SafeScopeName prevent nil pointer dereference.
3051
func (s Subject) SafeScopeName() string {
3152
if s.Scope == nil {

coderd/rbac/object.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,23 @@ type Object struct {
176176
ACLGroupList map[string][]Action ` json:"acl_group_list"`
177177
}
178178

179+
func (z Object) Equal(b Object) bool {
180+
if z.ID != b.ID {
181+
return false
182+
}
183+
if z.Owner != b.Owner {
184+
return false
185+
}
186+
if z.OrgID != b.OrgID {
187+
return false
188+
}
189+
if z.Type != b.Type {
190+
return false
191+
}
192+
// TODO: Handle ACLS
193+
return true
194+
}
195+
179196
func (z Object) RBACObject() Object {
180197
return z
181198
}

coderd/util/slice/slice.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
package slice
22

3+
// SameElements returns true if the 2 lists have the same elements in any
4+
// order.
5+
func SameElements[T comparable](a []T, b []T) bool {
6+
if len(a) != len(b) {
7+
return false
8+
}
9+
10+
for _, element := range a {
11+
if !Contains(b, element) {
12+
return false
13+
}
14+
}
15+
return true
16+
}
17+
318
func ContainsCompare[T any](haystack []T, needle T, equal func(a, b T) bool) bool {
419
for _, hay := range haystack {
520
if equal(needle, hay) {

0 commit comments

Comments
 (0)