Files
coder/coderd/rbac/authz_test.go
Steven Masley eb48341696 chore: More complete tracing for RBAC functions (#5690)
* chore: More complete tracing for RBAC functions
* Add input.json as example rbac input for rego cli

The input.json is required to play with the rego cli and debug
the policy without golang. It is good to have an example to run
the commands in the readme.md

* Add span events to capture authorize and prepared results
* chore: Add prometheus metrics to rbac authorizer
2023-01-13 16:07:15 -06:00

215 lines
7.2 KiB
Go

package rbac_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/rbac"
)
type benchmarkCase struct {
Name string
Roles []string
Groups []string
UserID uuid.UUID
Scope rbac.Scope
}
// benchmarkUserCases builds a set of users with different roles and groups.
// The user id used as the subject and the orgs used to build the roles are
// returned.
func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.UUID) {
orgs = []uuid.UUID{
uuid.MustParse("bf7b72bd-a2b1-4ef2-962c-1d698e0483f6"),
uuid.MustParse("e4660c6f-b9de-422d-9578-cd888983a795"),
uuid.MustParse("fb13d477-06f4-42d9-b957-f6b89bd63515"),
}
user := uuid.MustParse("10d03e62-7703-4df5-a358-4f76577d4e2f")
// noiseGroups are just added to add noise to the authorize call. They
// never match an object's list of groups.
noiseGroups := []string{uuid.NewString(), uuid.NewString(), uuid.NewString()}
benchCases := []benchmarkCase{
{
Name: "NoRoles",
Roles: []string{},
UserID: user,
Scope: rbac.ScopeAll,
},
{
Name: "Admin",
// Give some extra roles that an admin might have
Roles: []string{rbac.RoleOrgMember(orgs[0]), "auditor", rbac.RoleOwner(), rbac.RoleMember()},
UserID: user,
Scope: rbac.ScopeAll,
Groups: noiseGroups,
},
{
Name: "OrgAdmin",
Roles: []string{rbac.RoleOrgMember(orgs[0]), rbac.RoleOrgAdmin(orgs[0]), rbac.RoleMember()},
UserID: user,
Scope: rbac.ScopeAll,
Groups: noiseGroups,
},
{
Name: "OrgMember",
// Member of 2 orgs
Roles: []string{rbac.RoleOrgMember(orgs[0]), rbac.RoleOrgMember(orgs[1]), rbac.RoleMember()},
UserID: user,
Scope: rbac.ScopeAll,
Groups: noiseGroups,
},
{
Name: "ManyRoles",
// Admin of many orgs
Roles: []string{
rbac.RoleOrgMember(orgs[0]), rbac.RoleOrgAdmin(orgs[0]),
rbac.RoleOrgMember(orgs[1]), rbac.RoleOrgAdmin(orgs[1]),
rbac.RoleOrgMember(orgs[2]), rbac.RoleOrgAdmin(orgs[2]),
rbac.RoleMember(),
},
UserID: user,
Scope: rbac.ScopeAll,
Groups: noiseGroups,
},
{
Name: "AdminWithScope",
// Give some extra roles that an admin might have
Roles: []string{rbac.RoleOrgMember(orgs[0]), "auditor", rbac.RoleOwner(), rbac.RoleMember()},
UserID: user,
Scope: rbac.ScopeApplicationConnect,
Groups: noiseGroups,
},
}
return benchCases, users, orgs
}
// BenchmarkRBACAuthorize benchmarks the rbac.Authorize method.
//
// go test -bench BenchmarkRBACAuthorize -benchmem -memprofile memprofile.out -cpuprofile profile.out
func BenchmarkRBACAuthorize(b *testing.B) {
benchCases, user, orgs := benchmarkUserCases()
users := append([]uuid.UUID{},
user,
uuid.MustParse("4ca78b1d-f2d2-4168-9d76-cd93b51c6c1e"),
uuid.MustParse("0632b012-49e0-4d70-a5b3-f4398f1dcd52"),
uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"),
)
authorizer := rbac.NewAuthorizer(prometheus.NewRegistry())
// This benchmarks all the simple cases using just user permissions. Groups
// are added as noise, but do not do anything.
for _, c := range benchCases {
b.Run(c.Name, func(b *testing.B) {
objects := benchmarkSetup(orgs, users, b.N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
allowed := authorizer.ByRoleName(context.Background(), c.UserID.String(), c.Roles, c.Scope, c.Groups, rbac.ActionRead, objects[b.N%len(objects)])
var _ = allowed
}
})
}
}
// BenchmarkRBACAuthorizeGroups benchmarks the rbac.Authorize method and leverages
// groups for authorizing rather than the permissions/roles.
//
// go test -bench BenchmarkRBACAuthorizeGroups -benchmem -memprofile memprofile.out -cpuprofile profile.out
func BenchmarkRBACAuthorizeGroups(b *testing.B) {
benchCases, user, orgs := benchmarkUserCases()
users := append([]uuid.UUID{},
user,
uuid.MustParse("4ca78b1d-f2d2-4168-9d76-cd93b51c6c1e"),
uuid.MustParse("0632b012-49e0-4d70-a5b3-f4398f1dcd52"),
uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"),
)
authorizer := rbac.NewAuthorizer(prometheus.NewRegistry())
// Same benchmark cases, but this time groups will be used to match.
// Some '*' permissions will still match, but using a fake action reduces
// the chance.
neverMatchAction := rbac.Action("never-match-action")
for _, c := range benchCases {
b.Run(c.Name+"GroupACL", func(b *testing.B) {
userGroupAllow := uuid.NewString()
c.Groups = append(c.Groups, userGroupAllow)
c.Scope = rbac.ScopeAll
objects := benchmarkSetup(orgs, users, b.N, func(object rbac.Object) rbac.Object {
m := map[string][]rbac.Action{
// Add the user's group
// Noise
uuid.NewString(): {rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
uuid.NewString(): {rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate},
uuid.NewString(): {rbac.ActionCreate, rbac.ActionRead},
uuid.NewString(): {rbac.ActionCreate},
uuid.NewString(): {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
uuid.NewString(): {rbac.ActionRead, rbac.ActionUpdate},
}
for _, g := range c.Groups {
// Every group the user is in will be added, but it will not match the perms. This makes the
// authorizer look at many groups before finding the one that matches.
m[g] = []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}
}
// This is the only group that will give permission.
m[userGroupAllow] = []rbac.Action{neverMatchAction}
return object.WithGroupACL(m)
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
allowed := authorizer.ByRoleName(context.Background(), c.UserID.String(), c.Roles, c.Scope, c.Groups, neverMatchAction, objects[b.N%len(objects)])
var _ = allowed
}
})
}
}
// BenchmarkRBACFilter benchmarks the rbac.Filter method.
//
// go test -bench BenchmarkRBACFilter -benchmem -memprofile memprofile.out -cpuprofile profile.out
func BenchmarkRBACFilter(b *testing.B) {
benchCases, user, orgs := benchmarkUserCases()
users := append([]uuid.UUID{},
user,
uuid.MustParse("4ca78b1d-f2d2-4168-9d76-cd93b51c6c1e"),
uuid.MustParse("0632b012-49e0-4d70-a5b3-f4398f1dcd52"),
uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"),
)
authorizer := rbac.NewAuthorizer(prometheus.NewRegistry())
for _, c := range benchCases {
b.Run(c.Name, func(b *testing.B) {
objects := benchmarkSetup(orgs, users, b.N)
b.ResetTimer()
allowed, err := rbac.Filter(context.Background(), authorizer, c.UserID.String(), c.Roles, c.Scope, c.Groups, rbac.ActionRead, objects)
require.NoError(b, err)
var _ = allowed
})
}
}
func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int, opts ...func(object rbac.Object) rbac.Object) []rbac.Object {
// Create a "random" but deterministic set of objects.
aclList := map[string][]rbac.Action{
uuid.NewString(): {rbac.ActionRead, rbac.ActionUpdate},
uuid.NewString(): {rbac.ActionCreate},
}
objectList := make([]rbac.Object, size)
for i := range objectList {
objectList[i] = rbac.ResourceWorkspace.
InOrg(orgs[i%len(orgs)]).
WithOwner(users[i%len(users)].String()).
WithACLUserList(aclList).
WithGroupACL(aclList)
for _, opt := range opts {
objectList[i] = opt(objectList[i])
}
}
return objectList
}