feat: Implement RBAC checks on /templates endpoints (#1678)

* feat: Generic Filter method for rbac objects
This commit is contained in:
Steven Masley
2022-05-24 08:43:34 -05:00
committed by GitHub
parent fcd610ee7b
commit c7ca86d374
11 changed files with 221 additions and 73 deletions

View File

@ -3,7 +3,6 @@ package rbac
import (
"context"
_ "embed"
"golang.org/x/xerrors"
"github.com/open-policy-agent/opa/rego"
@ -13,6 +12,24 @@ type Authorizer interface {
ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error
}
// Filter takes in a list of objects, and will filter the list removing all
// the elements the subject does not have permission for.
// Filter does not allocate a new slice, and will use the existing one
// passed in. This can cause memory leaks if the slice is held for a prolonged
// period of time.
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, action Action, objects []O) []O {
filtered := make([]O, 0)
for i := range objects {
object := objects[i]
err := auth.ByRoleName(ctx, subjID, subjRoles, action, object.RBACObject())
if err == nil {
filtered = append(filtered, object)
}
}
return filtered
}
// RegoAuthorizer will use a prepared rego query for performing authorize()
type RegoAuthorizer struct {
query rego.PreparedEvalQuery

View File

@ -4,13 +4,12 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"
"testing"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/rbac"
)
@ -24,6 +23,94 @@ type subject struct {
Roles []rbac.Role `json:"roles"`
}
func TestFilter(t *testing.T) {
t.Parallel()
objectList := make([]rbac.Object, 0)
workspaceList := make([]rbac.Object, 0)
fileList := make([]rbac.Object, 0)
for i := 0; i < 10; i++ {
idxStr := strconv.Itoa(i)
workspace := rbac.ResourceWorkspace.WithID(idxStr).WithOwner("me")
file := rbac.ResourceFile.WithID(idxStr).WithOwner("me")
workspaceList = append(workspaceList, workspace)
fileList = append(fileList, file)
objectList = append(objectList, workspace)
objectList = append(objectList, file)
}
// copyList is to prevent tests from sharing the same slice
copyList := func(list []rbac.Object) []rbac.Object {
tmp := make([]rbac.Object, len(list))
copy(tmp, list)
return tmp
}
testCases := []struct {
Name string
List []rbac.Object
Expected []rbac.Object
Auth func(o rbac.Object) error
}{
{
Name: "FilterWorkspaceType",
List: copyList(objectList),
Expected: copyList(workspaceList),
Auth: func(o rbac.Object) error {
if o.Type != rbac.ResourceWorkspace.Type {
return xerrors.New("only workspace")
}
return nil
},
},
{
Name: "FilterFileType",
List: copyList(objectList),
Expected: copyList(fileList),
Auth: func(o rbac.Object) error {
if o.Type != rbac.ResourceFile.Type {
return xerrors.New("only file")
}
return nil
},
},
{
Name: "FilterAll",
List: copyList(objectList),
Expected: []rbac.Object{},
Auth: func(o rbac.Object) error {
return xerrors.New("always fail")
},
},
{
Name: "FilterNone",
List: copyList(objectList),
Expected: copyList(objectList),
Auth: func(o rbac.Object) error {
return nil
},
},
}
for _, c := range testCases {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
authorizer := fakeAuthorizer{
AuthFunc: func(_ context.Context, _ string, _ []string, _ rbac.Action, object rbac.Object) error {
return c.Auth(object)
},
}
filtered := rbac.Filter(context.Background(), authorizer, "me", []string{}, rbac.ActionRead, c.List)
require.ElementsMatch(t, c.Expected, filtered, "expect same list")
require.Equal(t, len(c.Expected), len(filtered), "same length list")
})
}
}
// TestAuthorizeDomain test the very basic roles that are commonly used.
func TestAuthorizeDomain(t *testing.T) {
t.Parallel()

15
coderd/rbac/fake_test.go Normal file
View File

@ -0,0 +1,15 @@
package rbac_test
import (
"context"
"github.com/coder/coder/coderd/rbac"
)
type fakeAuthorizer struct {
AuthFunc func(ctx context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error
}
func (f fakeAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
return f.AuthFunc(ctx, subjectID, roleNames, action, object)
}

View File

@ -6,6 +6,11 @@ import (
const WildcardSymbol = "*"
// Objecter returns the RBAC object for itself.
type Objecter interface {
RBACObject() Object
}
// Resources are just typed objects. Making resources this way allows directly
// passing them into an Authorize function and use the chaining api.
var (
@ -99,6 +104,10 @@ type Object struct {
// TODO: SharedUsers?
}
func (z Object) RBACObject() Object {
return z
}
// All returns an object matching all resources of the same type.
func (z Object) All() Object {
return Object{