mirror of https://github.com/coder/coder.git
488 lines
15 KiB
Go
488 lines
15 KiB
Go
package rbac_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/coderd/rbac"
|
|
)
|
|
|
|
// BenchmarkRBACFilter benchmarks the rbac.Filter method.
|
|
//
|
|
// go test -bench BenchmarkRBACFilter -benchmem -memprofile memprofile.out -cpuprofile profile.out
|
|
func BenchmarkRBACFilter(b *testing.B) {
|
|
orgs := []uuid.UUID{
|
|
uuid.MustParse("bf7b72bd-a2b1-4ef2-962c-1d698e0483f6"),
|
|
uuid.MustParse("e4660c6f-b9de-422d-9578-cd888983a795"),
|
|
uuid.MustParse("fb13d477-06f4-42d9-b957-f6b89bd63515"),
|
|
}
|
|
|
|
users := []uuid.UUID{
|
|
uuid.MustParse("10d03e62-7703-4df5-a358-4f76577d4e2f"),
|
|
uuid.MustParse("4ca78b1d-f2d2-4168-9d76-cd93b51c6c1e"),
|
|
uuid.MustParse("0632b012-49e0-4d70-a5b3-f4398f1dcd52"),
|
|
uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"),
|
|
}
|
|
|
|
benchCases := []struct {
|
|
Name string
|
|
Roles []string
|
|
UserID uuid.UUID
|
|
}{
|
|
{
|
|
Name: "NoRoles",
|
|
Roles: []string{},
|
|
UserID: users[0],
|
|
},
|
|
{
|
|
Name: "Admin",
|
|
// Give some extra roles that an admin might have
|
|
Roles: []string{rbac.RoleOrgMember(orgs[0]), "auditor", rbac.RoleOwner(), rbac.RoleMember()},
|
|
UserID: users[0],
|
|
},
|
|
{
|
|
Name: "OrgAdmin",
|
|
Roles: []string{rbac.RoleOrgMember(orgs[0]), rbac.RoleOrgAdmin(orgs[0]), rbac.RoleMember()},
|
|
UserID: users[0],
|
|
},
|
|
{
|
|
Name: "OrgMember",
|
|
// Member of 2 orgs
|
|
Roles: []string{rbac.RoleOrgMember(orgs[0]), rbac.RoleOrgMember(orgs[1]), rbac.RoleMember()},
|
|
UserID: users[0],
|
|
},
|
|
{
|
|
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: users[0],
|
|
},
|
|
}
|
|
|
|
authorizer, err := rbac.NewAuthorizer()
|
|
if err != nil {
|
|
require.NoError(b, err)
|
|
}
|
|
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, rbac.ActionRead, objects)
|
|
require.NoError(b, err)
|
|
var _ = allowed
|
|
})
|
|
}
|
|
}
|
|
|
|
func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int) []rbac.Object {
|
|
// Create a "random" but deterministic set of objects.
|
|
objectList := make([]rbac.Object, size)
|
|
for i := range objectList {
|
|
objectList[i] = rbac.ResourceWorkspace.
|
|
InOrg(orgs[i%len(orgs)]).
|
|
WithOwner(users[i%len(users)].String())
|
|
}
|
|
|
|
return objectList
|
|
}
|
|
|
|
type authSubject struct {
|
|
// Name is helpful for test assertions
|
|
Name string
|
|
UserID string
|
|
Roles []string
|
|
}
|
|
|
|
func TestRolePermissions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
auth, err := rbac.NewAuthorizer()
|
|
require.NoError(t, err, "new rego authorizer")
|
|
|
|
// currentUser is anything that references "me", "mine", or "my".
|
|
currentUser := uuid.New()
|
|
adminID := uuid.New()
|
|
templateAdminID := uuid.New()
|
|
orgID := uuid.New()
|
|
otherOrg := uuid.New()
|
|
|
|
// Subjects to user
|
|
memberMe := authSubject{Name: "member_me", UserID: currentUser.String(), Roles: []string{rbac.RoleMember()}}
|
|
orgMemberMe := authSubject{Name: "org_member_me", UserID: currentUser.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID)}}
|
|
|
|
owner := authSubject{Name: "owner", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOwner()}}
|
|
orgAdmin := authSubject{Name: "org_admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID), rbac.RoleOrgAdmin(orgID)}}
|
|
|
|
otherOrgMember := authSubject{Name: "org_member_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg)}}
|
|
otherOrgAdmin := authSubject{Name: "org_admin_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg), rbac.RoleOrgAdmin(otherOrg)}}
|
|
|
|
templateAdmin := authSubject{Name: "template-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}
|
|
userAdmin := authSubject{Name: "user-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleUserAdmin()}}
|
|
|
|
// requiredSubjects are required to be asserted in each test case. This is
|
|
// to make sure one is not forgotten.
|
|
requiredSubjects := []authSubject{memberMe, owner, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}
|
|
|
|
testCases := []struct {
|
|
// Name the test case to better locate the failing test case.
|
|
Name string
|
|
Resource rbac.Object
|
|
Actions []rbac.Action
|
|
// AuthorizeMap must cover all subjects in 'requiredSubjects'.
|
|
// This map will run an Authorize() check with the resource, action,
|
|
// and subjects. The subjects are split into 2 categories, "true" and
|
|
// "false".
|
|
// true: Subjects who Authorize should return no error
|
|
// false: Subjects who Authorize should return forbidden.
|
|
AuthorizeMap map[bool][]authSubject
|
|
}{
|
|
{
|
|
Name: "MyUser",
|
|
Actions: []rbac.Action{rbac.ActionRead},
|
|
Resource: rbac.ResourceUser,
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin, userAdmin},
|
|
false: {},
|
|
},
|
|
},
|
|
{
|
|
Name: "AUser",
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceUser,
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, userAdmin},
|
|
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "MyWorkspaceInOrg",
|
|
// When creating the WithID won't be set, but it does not change the result.
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(currentUser.String()),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgMemberMe, orgAdmin, templateAdmin},
|
|
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "MyWorkspaceInOrgExecution",
|
|
// When creating the WithID won't be set, but it does not change the result.
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceWorkspaceExecution.InOrg(orgID).WithOwner(currentUser.String()),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin, orgMemberMe},
|
|
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "Templates",
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceTemplate.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin, templateAdmin},
|
|
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "ReadTemplates",
|
|
Actions: []rbac.Action{rbac.ActionRead},
|
|
Resource: rbac.ResourceTemplate.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgMemberMe, orgAdmin, templateAdmin},
|
|
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "Files",
|
|
Actions: []rbac.Action{rbac.ActionCreate},
|
|
Resource: rbac.ResourceFile,
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, templateAdmin},
|
|
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "MyFile",
|
|
Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceFile.WithOwner(currentUser.String()),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, memberMe, orgMemberMe, templateAdmin},
|
|
false: {orgAdmin, otherOrgAdmin, otherOrgMember, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "CreateOrganizations",
|
|
Actions: []rbac.Action{rbac.ActionCreate},
|
|
Resource: rbac.ResourceOrganization,
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner},
|
|
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "Organizations",
|
|
Actions: []rbac.Action{rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceOrganization.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin},
|
|
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "ReadOrganizations",
|
|
Actions: []rbac.Action{rbac.ActionRead},
|
|
Resource: rbac.ResourceOrganization.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin, orgMemberMe},
|
|
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "RoleAssignment",
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceRoleAssignment,
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, userAdmin},
|
|
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "ReadRoleAssignment",
|
|
Actions: []rbac.Action{rbac.ActionRead},
|
|
Resource: rbac.ResourceRoleAssignment,
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
|
false: {},
|
|
},
|
|
},
|
|
{
|
|
Name: "OrgRoleAssignment",
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin},
|
|
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "ReadOrgRoleAssignment",
|
|
Actions: []rbac.Action{rbac.ActionRead},
|
|
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin, orgMemberMe},
|
|
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "APIKey",
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceAPIKey.WithOwner(currentUser.String()),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgMemberMe, memberMe},
|
|
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "UserData",
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceUserData.WithOwner(currentUser.String()),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgMemberMe, memberMe},
|
|
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "ManageOrgMember",
|
|
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
|
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin, userAdmin},
|
|
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "ReadOrgMember",
|
|
Actions: []rbac.Action{rbac.ActionRead},
|
|
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
|
|
AuthorizeMap: map[bool][]authSubject{
|
|
true: {owner, orgAdmin, orgMemberMe, userAdmin},
|
|
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range testCases {
|
|
c := c
|
|
t.Run(c.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
remainingSubjs := make(map[string]struct{})
|
|
for _, subj := range requiredSubjects {
|
|
remainingSubjs[subj.Name] = struct{}{}
|
|
}
|
|
|
|
for _, action := range c.Actions {
|
|
for result, subjs := range c.AuthorizeMap {
|
|
for _, subj := range subjs {
|
|
delete(remainingSubjs, subj.Name)
|
|
msg := fmt.Sprintf("%s as %q doing %q on %q", c.Name, subj.Name, action, c.Resource.Type)
|
|
err := auth.ByRoleName(context.Background(), subj.UserID, subj.Roles, action, c.Resource)
|
|
if result {
|
|
assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
|
|
} else {
|
|
assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
require.Empty(t, remainingSubjs, "test should cover all subjects")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsOrgRole(t *testing.T) {
|
|
t.Parallel()
|
|
randomUUID := uuid.New()
|
|
|
|
testCases := []struct {
|
|
RoleName string
|
|
OrgRole bool
|
|
OrgID string
|
|
}{
|
|
// Not org roles
|
|
{RoleName: rbac.RoleOwner()},
|
|
{RoleName: rbac.RoleMember()},
|
|
{RoleName: "auditor"},
|
|
|
|
{
|
|
RoleName: "a:bad:role",
|
|
OrgRole: false,
|
|
},
|
|
{
|
|
RoleName: "",
|
|
OrgRole: false,
|
|
},
|
|
|
|
// Org roles
|
|
{
|
|
RoleName: rbac.RoleOrgAdmin(randomUUID),
|
|
OrgRole: true,
|
|
OrgID: randomUUID.String(),
|
|
},
|
|
{
|
|
RoleName: rbac.RoleOrgMember(randomUUID),
|
|
OrgRole: true,
|
|
OrgID: randomUUID.String(),
|
|
},
|
|
{
|
|
RoleName: "test:example",
|
|
OrgRole: true,
|
|
OrgID: "example",
|
|
},
|
|
}
|
|
|
|
// nolint:paralleltest
|
|
for _, c := range testCases {
|
|
t.Run(c.RoleName, func(t *testing.T) {
|
|
t.Parallel()
|
|
orgID, ok := rbac.IsOrgRole(c.RoleName)
|
|
require.Equal(t, c.OrgRole, ok, "match expected org role")
|
|
require.Equal(t, c.OrgID, orgID, "match expected org id")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListRoles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
siteRoles := rbac.SiteRoles()
|
|
siteRoleNames := make([]string, 0, len(siteRoles))
|
|
for _, role := range siteRoles {
|
|
siteRoleNames = append(siteRoleNames, role.Name)
|
|
}
|
|
|
|
// If this test is ever failing, just update the list to the roles
|
|
// expected from the builtin set.
|
|
// Always use constant strings, as if the names change, we need to write
|
|
// a SQL migration to change the name on the backend.
|
|
require.ElementsMatch(t, []string{
|
|
"owner",
|
|
"member",
|
|
"auditor",
|
|
"template-admin",
|
|
"user-admin",
|
|
},
|
|
siteRoleNames)
|
|
|
|
orgID := uuid.New()
|
|
orgRoles := rbac.OrganizationRoles(orgID)
|
|
orgRoleNames := make([]string, 0, len(orgRoles))
|
|
for _, role := range orgRoles {
|
|
orgRoleNames = append(orgRoleNames, role.Name)
|
|
}
|
|
|
|
require.ElementsMatch(t, []string{
|
|
fmt.Sprintf("organization-admin:%s", orgID.String()),
|
|
fmt.Sprintf("organization-member:%s", orgID.String()),
|
|
},
|
|
orgRoleNames)
|
|
}
|
|
|
|
func TestChangeSet(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
Name string
|
|
From []string
|
|
To []string
|
|
ExpAdd []string
|
|
ExpRemove []string
|
|
}{
|
|
{
|
|
Name: "Empty",
|
|
},
|
|
{
|
|
Name: "Same",
|
|
From: []string{"a", "b", "c"},
|
|
To: []string{"a", "b", "c"},
|
|
ExpAdd: []string{},
|
|
ExpRemove: []string{},
|
|
},
|
|
{
|
|
Name: "AllRemoved",
|
|
From: []string{"a", "b", "c"},
|
|
ExpRemove: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
Name: "AllAdded",
|
|
To: []string{"a", "b", "c"},
|
|
ExpAdd: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
Name: "AddAndRemove",
|
|
From: []string{"a", "b", "c"},
|
|
To: []string{"a", "b", "d", "e"},
|
|
ExpAdd: []string{"d", "e"},
|
|
ExpRemove: []string{"c"},
|
|
},
|
|
}
|
|
|
|
for _, c := range testCases {
|
|
c := c
|
|
t.Run(c.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
add, remove := rbac.ChangeRoleSet(c.From, c.To)
|
|
require.ElementsMatch(t, c.ExpAdd, add, "expect added")
|
|
require.ElementsMatch(t, c.ExpRemove, remove, "expect removed")
|
|
})
|
|
}
|
|
}
|