Files
coder/coderd/rbac/roles_internal_test.go
Steven Masley cb6b5e8fbd chore: push rbac actions to policy package (#13274)
Just moved `rbac.Action` -> `policy.Action`. This is for the stacked PR to not have circular dependencies when doing autogen. Without this, the autogen can produce broken golang code, which prevents the autogen from compiling.

So just avoiding circular dependencies. Doing this in it's own PR to reduce LoC diffs in the primary PR, since this has 0 functional changes.
2024-05-15 09:46:35 -05:00

270 lines
8.2 KiB
Go

package rbac
import (
"testing"
"github.com/google/uuid"
"github.com/open-policy-agent/opa/ast"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
// BenchmarkRBACValueAllocation benchmarks the cost of allocating a rego input
// value. By default, `ast.InterfaceToValue` is used to convert the input,
// which uses json marshaling 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][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
}).WithACLUserList(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.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, policy.ActionRead, obj)
require.NoError(b, err)
}
})
b.Run("JSONRegoValue", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := ast.InterfaceToValue(map[string]interface{}{
"subject": jsonSubject,
"action": policy.ActionRead,
"object": obj,
})
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()
// Expand all roles and make sure we have a good copy.
// This is because these tests modify the roles, and we don't want to
// modify the original roles.
roles, err := RoleNames{RoleOrgMember(uuid.New()), RoleOrgAdmin(uuid.New()), RoleMember()}.Expand()
require.NoError(t, err, "failed to expand roles")
for i := range roles {
// If all cached values are nil, then the role will not use
// the shared cached value.
roles[i].cachedRegoValue = nil
}
actor := Subject{
Roles: Roles(roles),
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][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
}).WithACLUserList(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
})
action := policy.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]("4592dac5-0945-42fd-828d-a903957d3dbb")},
{Role: builtInRoles[orgAdmin]("24c100c5-1920-49c0-8c38-1b640ac4b38c")},
{Role: builtInRoles[orgAdmin]("4a00f697-0040-4079-b3ce-d24470281a62")},
{Role: builtInRoles[orgMember]("3293c50e-fa5d-414f-a461-01112a4dfb6f")},
{Role: builtInRoles[orgMember]("f88dd23d-bdbd-469d-b82e-36ee06c3d1e1")},
{Role: builtInRoles[orgMember]("02cfd2a5-016c-4d8d-8290-301f5f18023d")},
}
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)
}
}