mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
* chore: Optimize rego policy evaluation allocations Manually convert to ast.Value instead of using generic json.Marshal conversion. * Add a unit test that prevents regressions of rego input The optimized input is always compared to the normal json marshal parser.
253 lines
7.4 KiB
Go
253 lines
7.4 KiB
Go
package rbac
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// BenchmarkRBACValueAllocation benchmarks the cost of allocating a rego input
|
|
// value. By default, `ast.InterfaceToValue` is used to convert the input,
|
|
// which uses json marshalling under the hood.
|
|
//
|
|
// Currently ast.Object.insert() is the slowest part of the process and allocates
|
|
// the most amount of bytes. This general approach copies all of our struct
|
|
// data and uses a lot of extra memory for handling things like sort order.
|
|
// A possible large improvement would be to implement the ast.Value interface directly.
|
|
func BenchmarkRBACValueAllocation(b *testing.B) {
|
|
actor := Subject{
|
|
Roles: RoleNames{RoleOrgMember(uuid.New()), RoleOrgAdmin(uuid.New()), RoleMember()},
|
|
ID: uuid.NewString(),
|
|
Scope: ScopeAll,
|
|
Groups: []string{uuid.NewString(), uuid.NewString(), uuid.NewString()},
|
|
}
|
|
obj := ResourceTemplate.
|
|
WithID(uuid.New()).
|
|
InOrg(uuid.New()).
|
|
WithOwner(uuid.NewString()).
|
|
WithGroupACL(map[string][]Action{
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
}).WithACLUserList(map[string][]Action{
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
})
|
|
|
|
jsonSubject := authSubject{
|
|
ID: actor.ID,
|
|
Roles: must(actor.Roles.Expand()),
|
|
Groups: actor.Groups,
|
|
Scope: must(actor.Scope.Expand()),
|
|
}
|
|
|
|
b.Run("ManualRegoValue", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := regoInputValue(actor, ActionRead, obj)
|
|
require.NoError(b, err)
|
|
}
|
|
})
|
|
b.Run("JSONRegoValue", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := ast.InterfaceToValue(jsonSubject)
|
|
require.NoError(b, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRegoInputValue ensures the custom rego input parser returns the
|
|
// same value as the default json parser. The json parser is always correct,
|
|
// and the custom parser is used to reduce allocations. This optimization
|
|
// should yield the same results. Anything different is a bug.
|
|
func TestRegoInputValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
actor := Subject{
|
|
Roles: RoleNames{RoleOrgMember(uuid.New()), RoleOrgAdmin(uuid.New()), RoleMember()},
|
|
ID: uuid.NewString(),
|
|
Scope: ScopeAll,
|
|
Groups: []string{uuid.NewString(), uuid.NewString(), uuid.NewString()},
|
|
}
|
|
|
|
obj := ResourceTemplate.
|
|
WithID(uuid.New()).
|
|
InOrg(uuid.New()).
|
|
WithOwner(uuid.NewString()).
|
|
WithGroupACL(map[string][]Action{
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
}).WithACLUserList(map[string][]Action{
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
uuid.NewString(): {ActionRead, ActionCreate},
|
|
})
|
|
|
|
action := ActionRead
|
|
|
|
t.Run("InputValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// This is the input that would be passed to the rego policy.
|
|
jsonInput := map[string]interface{}{
|
|
"subject": authSubject{
|
|
ID: actor.ID,
|
|
Roles: must(actor.Roles.Expand()),
|
|
Groups: actor.Groups,
|
|
Scope: must(actor.Scope.Expand()),
|
|
},
|
|
"action": action,
|
|
"object": obj,
|
|
}
|
|
|
|
manual, err := regoInputValue(actor, action, obj)
|
|
require.NoError(t, err)
|
|
|
|
general, err := ast.InterfaceToValue(jsonInput)
|
|
require.NoError(t, err)
|
|
|
|
// The custom parser does not set these fields because they are not needed.
|
|
// To ensure the outputs are identical, intentionally overwrite all names
|
|
// to the same values.
|
|
ignoreNames(t, manual)
|
|
ignoreNames(t, general)
|
|
|
|
cmp := manual.Compare(general)
|
|
require.Equal(t, 0, cmp, "manual and general input values should be equal")
|
|
})
|
|
|
|
t.Run("PartialInputValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// This is the input that would be passed to the rego policy.
|
|
jsonInput := map[string]interface{}{
|
|
"subject": authSubject{
|
|
ID: actor.ID,
|
|
Roles: must(actor.Roles.Expand()),
|
|
Groups: actor.Groups,
|
|
Scope: must(actor.Scope.Expand()),
|
|
},
|
|
"action": action,
|
|
"object": map[string]interface{}{
|
|
"type": obj.Type,
|
|
},
|
|
}
|
|
|
|
manual, err := regoPartialInputValue(actor, action, obj.Type)
|
|
require.NoError(t, err)
|
|
|
|
general, err := ast.InterfaceToValue(jsonInput)
|
|
require.NoError(t, err)
|
|
|
|
// The custom parser does not set these fields because they are not needed.
|
|
// To ensure the outputs are identical, intentionally overwrite all names
|
|
// to the same values.
|
|
ignoreNames(t, manual)
|
|
ignoreNames(t, general)
|
|
|
|
cmp := manual.Compare(general)
|
|
require.Equal(t, 0, cmp, "manual and general input values should be equal")
|
|
})
|
|
}
|
|
|
|
// ignoreNames sets all names to "ignore" to ensure the values are identical.
|
|
func ignoreNames(t *testing.T, value ast.Value) {
|
|
t.Helper()
|
|
|
|
// Override the names of the roles
|
|
ref := ast.Ref{
|
|
ast.StringTerm("subject"),
|
|
ast.StringTerm("roles"),
|
|
}
|
|
roles, err := value.Find(ref)
|
|
require.NoError(t, err)
|
|
|
|
rolesArray, ok := roles.(*ast.Array)
|
|
require.True(t, ok, "roles is expected to be an array")
|
|
|
|
rolesArray.Foreach(func(term *ast.Term) {
|
|
obj, _ := term.Value.(ast.Object)
|
|
// Ignore all names
|
|
obj.Insert(ast.StringTerm("name"), ast.StringTerm("ignore"))
|
|
obj.Insert(ast.StringTerm("display_name"), ast.StringTerm("ignore"))
|
|
})
|
|
|
|
// Override the names of the scope role
|
|
ref = ast.Ref{
|
|
ast.StringTerm("subject"),
|
|
ast.StringTerm("scope"),
|
|
}
|
|
scope, err := value.Find(ref)
|
|
require.NoError(t, err)
|
|
|
|
scopeObj, ok := scope.(ast.Object)
|
|
require.True(t, ok, "scope is expected to be an object")
|
|
|
|
scopeObj.Insert(ast.StringTerm("name"), ast.StringTerm("ignore"))
|
|
scopeObj.Insert(ast.StringTerm("display_name"), ast.StringTerm("ignore"))
|
|
}
|
|
|
|
func TestRoleByName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("BuiltIns", func(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
Role Role
|
|
}{
|
|
{Role: builtInRoles[owner]("")},
|
|
{Role: builtInRoles[member]("")},
|
|
{Role: builtInRoles[templateAdmin]("")},
|
|
{Role: builtInRoles[userAdmin]("")},
|
|
{Role: builtInRoles[auditor]("")},
|
|
|
|
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
|
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
|
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
|
|
|
{Role: builtInRoles[orgMember](uuid.New().String())},
|
|
{Role: builtInRoles[orgMember](uuid.New().String())},
|
|
{Role: builtInRoles[orgMember](uuid.New().String())},
|
|
}
|
|
|
|
for _, c := range testCases {
|
|
c := c
|
|
t.Run(c.Role.Name, func(t *testing.T) {
|
|
role, err := RoleByName(c.Role.Name)
|
|
require.NoError(t, err, "role exists")
|
|
equalRoles(t, c.Role, role)
|
|
})
|
|
}
|
|
})
|
|
|
|
// nolint:paralleltest
|
|
t.Run("Errors", func(t *testing.T) {
|
|
var err error
|
|
|
|
_, err = RoleByName("")
|
|
require.Error(t, err, "empty role")
|
|
|
|
_, err = RoleByName("too:many:colons")
|
|
require.Error(t, err, "too many colons")
|
|
|
|
_, err = RoleByName(orgMember)
|
|
require.Error(t, err, "expect orgID")
|
|
})
|
|
}
|
|
|
|
// SameAs compares 2 roles for equality.
|
|
func equalRoles(t *testing.T, a, b Role) {
|
|
require.Equal(t, a.Name, b.Name, "role names")
|
|
require.Equal(t, a.DisplayName, b.DisplayName, "role display names")
|
|
require.ElementsMatch(t, a.Site, b.Site, "site permissions")
|
|
require.ElementsMatch(t, a.User, b.User, "user permissions")
|
|
require.Equal(t, len(a.Org), len(b.Org), "same number of org roles")
|
|
|
|
for ak, av := range a.Org {
|
|
bv, ok := b.Org[ak]
|
|
require.True(t, ok, "org permissions missing: %s", ak)
|
|
require.ElementsMatchf(t, av, bv, "org %s permissions", ak)
|
|
}
|
|
}
|