mirror of https://github.com/coder/coder.git
chore: Allow RecordingAuthorizer to record multiple rbac authz calls (#6024)
* chore: Allow RecordingAuthorizer to record multiple rbac authz calls Prior iteration only recorded the last call. This is required for more comprehensive testing
This commit is contained in:
parent
571f5d0e02
commit
b359dbbd8b
|
@ -7,17 +7,17 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd/database/dbfake"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/database/dbfake"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/rbac/regosql"
|
||||
"github.com/coder/coder/codersdk"
|
||||
|
@ -443,7 +443,9 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
|
|||
|
||||
func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck, skipRoutes map[string]string) {
|
||||
// Always fail auth from this point forward
|
||||
a.authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
|
||||
a.authorizer.Wrapped = &FakeAuthorizer{
|
||||
AlwaysReturn: rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil),
|
||||
}
|
||||
|
||||
routeMissing := make(map[string]bool)
|
||||
for k, v := range assertRoute {
|
||||
|
@ -483,7 +485,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
|
|||
return nil
|
||||
}
|
||||
a.t.Run(name, func(t *testing.T) {
|
||||
a.authorizer.reset()
|
||||
a.authorizer.Reset()
|
||||
routeKey := strings.TrimRight(name, "/")
|
||||
|
||||
routeAssertions, ok := assertRoute[routeKey]
|
||||
|
@ -514,18 +516,19 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
|
|||
assert.Equal(t, http.StatusForbidden, resp.StatusCode, "expect unauthorized")
|
||||
}
|
||||
}
|
||||
if a.authorizer.Called != nil {
|
||||
if a.authorizer.lastCall() != nil {
|
||||
last := a.authorizer.lastCall()
|
||||
if routeAssertions.AssertAction != "" {
|
||||
assert.Equal(t, routeAssertions.AssertAction, a.authorizer.Called.Action, "resource action")
|
||||
assert.Equal(t, routeAssertions.AssertAction, last.Action, "resource action")
|
||||
}
|
||||
if routeAssertions.AssertObject.Type != "" {
|
||||
assert.Equal(t, routeAssertions.AssertObject.Type, a.authorizer.Called.Object.Type, "resource type")
|
||||
assert.Equal(t, routeAssertions.AssertObject.Type, last.Object.Type, "resource type")
|
||||
}
|
||||
if routeAssertions.AssertObject.Owner != "" {
|
||||
assert.Equal(t, routeAssertions.AssertObject.Owner, a.authorizer.Called.Object.Owner, "resource owner")
|
||||
assert.Equal(t, routeAssertions.AssertObject.Owner, last.Object.Owner, "resource owner")
|
||||
}
|
||||
if routeAssertions.AssertObject.OrgID != "" {
|
||||
assert.Equal(t, routeAssertions.AssertObject.OrgID, a.authorizer.Called.Object.OrgID, "resource org")
|
||||
assert.Equal(t, routeAssertions.AssertObject.OrgID, last.Object.OrgID, "resource org")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -539,52 +542,195 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
|
|||
}
|
||||
|
||||
type authCall struct {
|
||||
Subject rbac.Subject
|
||||
Action rbac.Action
|
||||
Object rbac.Object
|
||||
}
|
||||
Actor rbac.Subject
|
||||
Action rbac.Action
|
||||
Object rbac.Object
|
||||
|
||||
type RecordingAuthorizer struct {
|
||||
Called *authCall
|
||||
AlwaysReturn error
|
||||
asserted bool
|
||||
}
|
||||
|
||||
var _ rbac.Authorizer = (*RecordingAuthorizer)(nil)
|
||||
|
||||
// AuthorizeSQL does not record the call. This matches the postgres behavior
|
||||
// of not calling Authorize()
|
||||
func (r *RecordingAuthorizer) AuthorizeSQL(_ context.Context, _ rbac.Subject, _ rbac.Action, _ rbac.Object) error {
|
||||
return r.AlwaysReturn
|
||||
// RecordingAuthorizer wraps any rbac.Authorizer and records all Authorize()
|
||||
// calls made. This is useful for testing as these calls can later be asserted.
|
||||
type RecordingAuthorizer struct {
|
||||
sync.RWMutex
|
||||
Called []authCall
|
||||
Wrapped rbac.Authorizer
|
||||
}
|
||||
|
||||
func (r *RecordingAuthorizer) Authorize(_ context.Context, subject rbac.Subject, action rbac.Action, object rbac.Object) error {
|
||||
r.Called = &authCall{
|
||||
Subject: subject,
|
||||
Action: action,
|
||||
Object: object,
|
||||
type ActionObjectPair struct {
|
||||
Action rbac.Action
|
||||
Object rbac.Object
|
||||
}
|
||||
|
||||
// Pair is on the RecordingAuthorizer to be easy to find and keep the pkg
|
||||
// interface smaller.
|
||||
func (*RecordingAuthorizer) Pair(action rbac.Action, object rbac.Objecter) ActionObjectPair {
|
||||
return ActionObjectPair{
|
||||
Action: action,
|
||||
Object: object.RBACObject(),
|
||||
}
|
||||
return r.AlwaysReturn
|
||||
}
|
||||
|
||||
func (r *RecordingAuthorizer) Prepare(_ context.Context, subject rbac.Subject, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
|
||||
return &fakePreparedAuthorizer{
|
||||
Original: r,
|
||||
Subject: subject,
|
||||
Action: action,
|
||||
HardCodedSQLString: "true",
|
||||
// AllAsserted returns an error if all calls to Authorize() have not been
|
||||
// asserted and checked. This is useful for testing to ensure that all
|
||||
// Authorize() calls are checked in the unit test.
|
||||
func (r *RecordingAuthorizer) AllAsserted() error {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
missed := []authCall{}
|
||||
for _, c := range r.Called {
|
||||
if !c.asserted {
|
||||
missed = append(missed, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missed) > 0 {
|
||||
return xerrors.Errorf("missed calls: %+v", missed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertActor asserts in order. If the order of authz calls does not match,
|
||||
// this will fail.
|
||||
func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
ptr := 0
|
||||
for i, call := range r.Called {
|
||||
if ptr == len(did) {
|
||||
// Finished all assertions
|
||||
return
|
||||
}
|
||||
if call.Actor.ID == actor.ID {
|
||||
action, object := did[ptr].Action, did[ptr].Object
|
||||
assert.Equalf(t, action, call.Action, "assert action %d", ptr)
|
||||
assert.Equalf(t, object, call.Object, "assert object %d", ptr)
|
||||
r.Called[i].asserted = true
|
||||
ptr++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equalf(t, len(did), ptr, "assert actor: didn't find all actions, %d missing actions", len(did)-ptr)
|
||||
}
|
||||
|
||||
// recordAuthorize is the internal method that records the Authorize() call.
|
||||
func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action rbac.Action, object rbac.Object) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.Called = append(r.Called, authCall{
|
||||
Actor: subject,
|
||||
Action: action,
|
||||
Object: object,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RecordingAuthorizer) Authorize(ctx context.Context, subject rbac.Subject, action rbac.Action, object rbac.Object) error {
|
||||
r.recordAuthorize(subject, action, object)
|
||||
if r.Wrapped == nil {
|
||||
panic("Developer error: RecordingAuthorizer.Wrapped is nil")
|
||||
}
|
||||
return r.Wrapped.Authorize(ctx, subject, action, object)
|
||||
}
|
||||
|
||||
func (r *RecordingAuthorizer) Prepare(ctx context.Context, subject rbac.Subject, action rbac.Action, objectType string) (rbac.PreparedAuthorized, error) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
if r.Wrapped == nil {
|
||||
panic("Developer error: RecordingAuthorizer.Wrapped is nil")
|
||||
}
|
||||
|
||||
prep, err := r.Wrapped.Prepare(ctx, subject, action, objectType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PreparedRecorder{
|
||||
rec: r,
|
||||
prepped: prep,
|
||||
subject: subject,
|
||||
action: action,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RecordingAuthorizer) reset() {
|
||||
// Reset clears the recorded Authorize() calls.
|
||||
func (r *RecordingAuthorizer) Reset() {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.Called = nil
|
||||
}
|
||||
|
||||
// lastCall is implemented to support legacy tests.
|
||||
// Deprecated
|
||||
func (r *RecordingAuthorizer) lastCall() *authCall {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
if len(r.Called) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &r.Called[len(r.Called)-1]
|
||||
}
|
||||
|
||||
// PreparedRecorder is the prepared version of the RecordingAuthorizer.
|
||||
// It records the Authorize() calls to the original recorder. If the caller
|
||||
// uses CompileToSQL, all recording stops. This is to support parity between
|
||||
// memory and SQL backed dbs.
|
||||
type PreparedRecorder struct {
|
||||
rec *RecordingAuthorizer
|
||||
prepped rbac.PreparedAuthorized
|
||||
subject rbac.Subject
|
||||
action rbac.Action
|
||||
|
||||
rw sync.Mutex
|
||||
usingSQL bool
|
||||
}
|
||||
|
||||
func (s *PreparedRecorder) Authorize(ctx context.Context, object rbac.Object) error {
|
||||
s.rw.Lock()
|
||||
defer s.rw.Unlock()
|
||||
|
||||
if !s.usingSQL {
|
||||
s.rec.recordAuthorize(s.subject, s.action, object)
|
||||
}
|
||||
return s.prepped.Authorize(ctx, object)
|
||||
}
|
||||
func (s *PreparedRecorder) CompileToSQL(ctx context.Context, cfg regosql.ConvertConfig) (string, error) {
|
||||
s.rw.Lock()
|
||||
defer s.rw.Unlock()
|
||||
|
||||
s.usingSQL = true
|
||||
return s.prepped.CompileToSQL(ctx, cfg)
|
||||
}
|
||||
|
||||
// FakeAuthorizer is an Authorizer that always returns the same error.
|
||||
type FakeAuthorizer struct {
|
||||
// AlwaysReturn is the error that will be returned by Authorize.
|
||||
AlwaysReturn error
|
||||
}
|
||||
|
||||
var _ rbac.Authorizer = (*FakeAuthorizer)(nil)
|
||||
|
||||
func (d *FakeAuthorizer) Authorize(_ context.Context, _ rbac.Subject, _ rbac.Action, _ rbac.Object) error {
|
||||
return d.AlwaysReturn
|
||||
}
|
||||
|
||||
func (d *FakeAuthorizer) Prepare(_ context.Context, subject rbac.Subject, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
|
||||
return &fakePreparedAuthorizer{
|
||||
Original: d,
|
||||
Subject: subject,
|
||||
Action: action,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ rbac.PreparedAuthorized = (*fakePreparedAuthorizer)(nil)
|
||||
|
||||
// fakePreparedAuthorizer is the prepared version of a FakeAuthorizer. It will
|
||||
// return the same error as the original FakeAuthorizer.
|
||||
type fakePreparedAuthorizer struct {
|
||||
Original *RecordingAuthorizer
|
||||
Subject rbac.Subject
|
||||
Action rbac.Action
|
||||
HardCodedSQLString string
|
||||
HardCodedRegoString string
|
||||
sync.RWMutex
|
||||
Original *FakeAuthorizer
|
||||
Subject rbac.Subject
|
||||
Action rbac.Action
|
||||
}
|
||||
|
||||
func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
|
||||
|
@ -593,17 +739,6 @@ func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Obje
|
|||
|
||||
// CompileToSQL returns a compiled version of the authorizer that will work for
|
||||
// in memory databases. This fake version will not work against a SQL database.
|
||||
func (fakePreparedAuthorizer) CompileToSQL(_ context.Context, _ regosql.ConvertConfig) (string, error) {
|
||||
return "", xerrors.New("not implemented")
|
||||
}
|
||||
|
||||
func (f *fakePreparedAuthorizer) Eval(object rbac.Object) bool {
|
||||
return f.Original.AuthorizeSQL(context.Background(), f.Subject, f.Action, object) == nil
|
||||
}
|
||||
|
||||
func (f fakePreparedAuthorizer) RegoString() string {
|
||||
if f.HardCodedRegoString != "" {
|
||||
return f.HardCodedRegoString
|
||||
}
|
||||
panic("not implemented")
|
||||
func (*fakePreparedAuthorizer) CompileToSQL(_ context.Context, _ regosql.ConvertConfig) (string, error) {
|
||||
return "not a valid sql string", nil
|
||||
}
|
||||
|
|
|
@ -4,7 +4,11 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
)
|
||||
|
||||
func TestAuthorizeAllEndpoints(t *testing.T) {
|
||||
|
@ -12,7 +16,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
||||
// Required for any subdomain-based proxy tests to pass.
|
||||
AppHostname: "*.test.coder.com",
|
||||
Authorizer: &coderdtest.RecordingAuthorizer{},
|
||||
Authorizer: &coderdtest.RecordingAuthorizer{Wrapped: &coderdtest.FakeAuthorizer{}},
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
admin := coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -20,3 +24,111 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
skipRoute, assertRoute := coderdtest.AGPLRoutes(a)
|
||||
a.Test(context.Background(), assertRoute, skipRoute)
|
||||
}
|
||||
|
||||
func TestAuthzRecorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Authorize", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{},
|
||||
}
|
||||
sub := randomSubject()
|
||||
pairs := fuzzAuthz(t, sub, rec, 10)
|
||||
rec.AssertActor(t, sub, pairs...)
|
||||
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
|
||||
})
|
||||
|
||||
t.Run("Authorize2Subjects", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{},
|
||||
}
|
||||
a := randomSubject()
|
||||
aPairs := fuzzAuthz(t, a, rec, 10)
|
||||
|
||||
b := randomSubject()
|
||||
bPairs := fuzzAuthz(t, b, rec, 10)
|
||||
|
||||
rec.AssertActor(t, b, bPairs...)
|
||||
rec.AssertActor(t, a, aPairs...)
|
||||
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
|
||||
})
|
||||
|
||||
t.Run("Authorize&Prepared", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{},
|
||||
}
|
||||
a := randomSubject()
|
||||
aPairs := fuzzAuthz(t, a, rec, 10)
|
||||
|
||||
b := randomSubject()
|
||||
|
||||
act, objTy := randomAction(), randomObject().Type
|
||||
prep, _ := rec.Prepare(context.Background(), b, act, objTy)
|
||||
bPairs := fuzzAuthzPrep(t, prep, 10, act, objTy)
|
||||
|
||||
rec.AssertActor(t, b, bPairs...)
|
||||
rec.AssertActor(t, a, aPairs...)
|
||||
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
|
||||
})
|
||||
}
|
||||
|
||||
// fuzzAuthzPrep has same action and object types for all calls.
|
||||
func fuzzAuthzPrep(t *testing.T, prep rbac.PreparedAuthorized, n int, action rbac.Action, objectType string) []coderdtest.ActionObjectPair {
|
||||
t.Helper()
|
||||
pairs := make([]coderdtest.ActionObjectPair, 0, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
obj := randomObject()
|
||||
obj.Type = objectType
|
||||
p := coderdtest.ActionObjectPair{Action: action, Object: obj}
|
||||
_ = prep.Authorize(context.Background(), p.Object)
|
||||
pairs = append(pairs, p)
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
func fuzzAuthz(t *testing.T, sub rbac.Subject, rec rbac.Authorizer, n int) []coderdtest.ActionObjectPair {
|
||||
t.Helper()
|
||||
pairs := make([]coderdtest.ActionObjectPair, 0, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
p := coderdtest.ActionObjectPair{Action: randomAction(), Object: randomObject()}
|
||||
_ = rec.Authorize(context.Background(), sub, p.Action, p.Object)
|
||||
pairs = append(pairs, p)
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
func randomAction() rbac.Action {
|
||||
return rbac.Action(namesgenerator.GetRandomName(1))
|
||||
}
|
||||
|
||||
func randomObject() rbac.Object {
|
||||
return rbac.Object{
|
||||
ID: namesgenerator.GetRandomName(1),
|
||||
Owner: namesgenerator.GetRandomName(1),
|
||||
OrgID: namesgenerator.GetRandomName(1),
|
||||
Type: namesgenerator.GetRandomName(1),
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
namesgenerator.GetRandomName(1): {rbac.ActionRead},
|
||||
},
|
||||
ACLGroupList: map[string][]rbac.Action{
|
||||
namesgenerator.GetRandomName(1): {rbac.ActionRead},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func randomSubject() rbac.Subject {
|
||||
return rbac.Subject{
|
||||
ID: namesgenerator.GetRandomName(1),
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Groups: []string{namesgenerator.GetRandomName(1)},
|
||||
Scope: rbac.ScopeAll,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/coder/coder/coderd/rbac/regosql"
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
"github.com/coder/coder/coderd/util/slice"
|
||||
)
|
||||
|
||||
// Subject is a struct that contains all the elements of a subject in an rbac
|
||||
|
@ -26,6 +27,25 @@ type Subject struct {
|
|||
Scope ExpandableScope
|
||||
}
|
||||
|
||||
func (s Subject) Equal(b Subject) bool {
|
||||
if s.ID != b.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
if !slice.SameElements(s.Groups, b.Groups) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !slice.SameElements(s.SafeRoleNames(), b.SafeRoleNames()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.SafeScopeName() != b.SafeScopeName() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SafeScopeName prevent nil pointer dereference.
|
||||
func (s Subject) SafeScopeName() string {
|
||||
if s.Scope == nil {
|
||||
|
|
|
@ -176,6 +176,49 @@ type Object struct {
|
|||
ACLGroupList map[string][]Action ` json:"acl_group_list"`
|
||||
}
|
||||
|
||||
func (z Object) Equal(b Object) bool {
|
||||
if z.ID != b.ID {
|
||||
return false
|
||||
}
|
||||
if z.Owner != b.Owner {
|
||||
return false
|
||||
}
|
||||
if z.OrgID != b.OrgID {
|
||||
return false
|
||||
}
|
||||
if z.Type != b.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
if !equalACLLists(z.ACLUserList, b.ACLUserList) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !equalACLLists(z.ACLGroupList, b.ACLGroupList) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func equalACLLists(a, b map[string][]Action) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, actions := range a {
|
||||
if len(actions) != len(b[k]) {
|
||||
return false
|
||||
}
|
||||
for i, a := range actions {
|
||||
if a != b[k][i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (z Object) RBACObject() Object {
|
||||
return z
|
||||
}
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
package rbac_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
)
|
||||
|
||||
func TestObjectEqual(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
Name string
|
||||
A rbac.Object
|
||||
B rbac.Object
|
||||
Expected bool
|
||||
}{
|
||||
{
|
||||
Name: "Empty",
|
||||
A: rbac.Object{},
|
||||
B: rbac.Object{},
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "NilVs0",
|
||||
A: rbac.Object{
|
||||
ACLGroupList: map[string][]rbac.Action{},
|
||||
ACLUserList: map[string][]rbac.Action{},
|
||||
},
|
||||
B: rbac.Object{},
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "Same",
|
||||
A: rbac.Object{
|
||||
ID: "id",
|
||||
Owner: "owner",
|
||||
OrgID: "orgID",
|
||||
Type: "type",
|
||||
ACLUserList: map[string][]rbac.Action{},
|
||||
ACLGroupList: map[string][]rbac.Action{},
|
||||
},
|
||||
B: rbac.Object{
|
||||
ID: "id",
|
||||
Owner: "owner",
|
||||
OrgID: "orgID",
|
||||
Type: "type",
|
||||
ACLUserList: map[string][]rbac.Action{},
|
||||
ACLGroupList: map[string][]rbac.Action{},
|
||||
},
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "DifferentID",
|
||||
A: rbac.Object{
|
||||
ID: "id",
|
||||
},
|
||||
B: rbac.Object{
|
||||
ID: "id2",
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "DifferentOwner",
|
||||
A: rbac.Object{
|
||||
Owner: "owner",
|
||||
},
|
||||
B: rbac.Object{
|
||||
Owner: "owner2",
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "DifferentOrgID",
|
||||
A: rbac.Object{
|
||||
OrgID: "orgID",
|
||||
},
|
||||
B: rbac.Object{
|
||||
OrgID: "orgID2",
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "DifferentType",
|
||||
A: rbac.Object{
|
||||
Type: "type",
|
||||
},
|
||||
B: rbac.Object{
|
||||
Type: "type2",
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "DifferentACLUserList",
|
||||
A: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user1": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
B: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user2": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "ACLUserDiff#Actions",
|
||||
A: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user1": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
B: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user1": {rbac.ActionRead, rbac.ActionUpdate},
|
||||
},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "ACLUserDiffAction",
|
||||
A: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user1": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
B: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user1": {rbac.ActionUpdate},
|
||||
},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "ACLUserDiff#Users",
|
||||
A: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user1": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
B: rbac.Object{
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
"user1": {rbac.ActionRead},
|
||||
"user2": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "DifferentACLGroupList",
|
||||
A: rbac.Object{
|
||||
ACLGroupList: map[string][]rbac.Action{
|
||||
"group1": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
B: rbac.Object{
|
||||
ACLGroupList: map[string][]rbac.Action{
|
||||
"group2": {rbac.ActionRead},
|
||||
},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := tc.A.Equal(tc.B)
|
||||
if actual != tc.Expected {
|
||||
t.Errorf("expected %v, got %v", tc.Expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package rbac_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
)
|
||||
|
||||
func TestSubjectEqual(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
Name string
|
||||
A rbac.Subject
|
||||
B rbac.Subject
|
||||
Expected bool
|
||||
}{
|
||||
{
|
||||
Name: "Empty",
|
||||
A: rbac.Subject{},
|
||||
B: rbac.Subject{},
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "Same",
|
||||
A: rbac.Subject{
|
||||
ID: "id",
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Groups: []string{"group"},
|
||||
Scope: rbac.ScopeAll,
|
||||
},
|
||||
B: rbac.Subject{
|
||||
ID: "id",
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Groups: []string{"group"},
|
||||
Scope: rbac.ScopeAll,
|
||||
},
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "DifferentID",
|
||||
A: rbac.Subject{
|
||||
ID: "id",
|
||||
},
|
||||
B: rbac.Subject{
|
||||
ID: "id2",
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "RolesNilVs0",
|
||||
A: rbac.Subject{
|
||||
Roles: rbac.RoleNames{},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Roles: nil,
|
||||
},
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "GroupsNilVs0",
|
||||
A: rbac.Subject{
|
||||
Groups: []string{},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Groups: nil,
|
||||
},
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "DifferentRoles",
|
||||
A: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "Different#Roles",
|
||||
A: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleOwner()},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "DifferentGroups",
|
||||
A: rbac.Subject{
|
||||
Groups: []string{"group1"},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Groups: []string{"group2"},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "Different#Groups",
|
||||
A: rbac.Subject{
|
||||
Groups: []string{"group1"},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Groups: []string{"group1", "group2"},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "DifferentScope",
|
||||
A: rbac.Subject{
|
||||
Scope: rbac.ScopeAll,
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Scope: rbac.ScopeApplicationConnect,
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := tc.A.Equal(tc.B)
|
||||
if actual != tc.Expected {
|
||||
t.Errorf("expected %v, got %v", tc.Expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,5 +1,25 @@
|
|||
package slice
|
||||
|
||||
// New is a convenience method for creating []T.
|
||||
func New[T any](items ...T) []T {
|
||||
return items
|
||||
}
|
||||
|
||||
// SameElements returns true if the 2 lists have the same elements in any
|
||||
// order.
|
||||
func SameElements[T comparable](a []T, b []T) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, element := range a {
|
||||
if !Contains(b, element) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ContainsCompare[T any](haystack []T, needle T, equal func(a, b T) bool) bool {
|
||||
for _, hay := range haystack {
|
||||
if equal(needle, hay) {
|
||||
|
|
|
@ -1,14 +1,43 @@
|
|||
package slice_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/util/slice"
|
||||
)
|
||||
|
||||
func TestSameElements(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// True
|
||||
assertSameElements(t, []int{})
|
||||
assertSameElements(t, []int{1, 2, 3})
|
||||
assertSameElements(t, slice.New("a", "b", "c"))
|
||||
assertSameElements(t, slice.New(uuid.New(), uuid.New(), uuid.New()))
|
||||
|
||||
// False
|
||||
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{1, 2, 3, 4}))
|
||||
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{1, 2}))
|
||||
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{}))
|
||||
assert.False(t, slice.SameElements([]int{}, []int{1, 2, 3}))
|
||||
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{1, 2, 4}))
|
||||
assert.False(t, slice.SameElements([]int{1}, []int{2}))
|
||||
}
|
||||
|
||||
func assertSameElements[T comparable](t *testing.T, elements []T) {
|
||||
cpy := make([]T, len(elements))
|
||||
copy(cpy, elements)
|
||||
rand.Shuffle(len(cpy), func(i, j int) {
|
||||
cpy[i], cpy[j] = cpy[j], cpy[i]
|
||||
})
|
||||
assert.True(t, slice.SameElements(elements, cpy))
|
||||
}
|
||||
|
||||
func TestUnique(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
Options: &coderdtest.Options{
|
||||
// Required for any subdomain-based proxy tests to pass.
|
||||
AppHostname: "*.test.coder.com",
|
||||
Authorizer: &coderdtest.RecordingAuthorizer{},
|
||||
Authorizer: &coderdtest.RecordingAuthorizer{Wrapped: &coderdtest.FakeAuthorizer{}},
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue