8000 test: Unit test to assert role capabilities by Emyrk · Pull Request #1781 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

test: Unit test to assert role capabilities #1781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 27, 2022
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 234 additions & 1 deletion coderd/rbac/builtin_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,249 @@
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"
)

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")

meID := uuid.New()
adminID := uuid.New()
orgID := uuid.New()
otherOrg := uuid.New()

// Subjects to user
member := authSubject{Name: "member", UserID: meID.String(), Roles: []string{rbac.RoleMember()}}
admin := authSubject{Name: "admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleAdmin()}}

orgMember := authSubject{Name: "org_member", UserID: meID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID)}}
orgAdmin := authSubject{Name: "org_admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID), rbac.RoleOrgAdmin(orgID)}}
otherOrgMember := authSubject{Name: "other_org_member", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg)}}
otherOrgAdmin := authSubject{Name: "other_org_admin", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg), rbac.RoleOrgAdmin(otherOrg)}}

// requiredSubjects are required to be asserted in each test case. This is
// to make sure one is not forgotten.
requiredSubjects := []authSubject{member, admin, orgMember, orgAdmin, otherOrgAdmin, otherOrgMember}

testCases := []struct {
// Name the test case to better locate the failing test case.
Name string
Resource rbac.Object
Actions []rbac.Action
// Assertions must cover all subjects in 'requiredSubjects'
Assertions map[bool][]authSubject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: map[authSubject]bool? Kind of easier to reason about. Optional though, this is really a nit pick.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt it was easier to just list out subjects in a 1 line list.

}{
{
Name: "MyUser",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceUser.WithID(meID.String()),
Assertions: map[bool][]authSubject{
true: {admin, member, orgMember, orgAdmin, otherOrgMember, otherOrgAdmin},
false: {},
},
},
{
Name: "AUser",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceUser,
Assertions: map[bool][]authSubject{
true: {admin},
false: {member, orgMember, orgAdmin, otherOrgMember, otherOrgAdmin},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Could we rename these fields for clarity? When I initially read the test I got a bit confused, as it appears to me that this test was asserting that any random user resource should have the admin role. Now after reading it, it appears that the Resource in question is the object, and the subjects are all of those identified in assertions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. I can rename. I changed this struct up a few times, probably why it ended as it did.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments too.

},
{
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(meID.String()).WithID(uuid.NewString()),
Assertions: map[bool][]authSubject{
true: {admin, orgMember, orgAdmin},
false: {member, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "Templates",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceTemplate.InOrg(orgID).WithID(uuid.NewString()),
Assertions: map[bool][]authSubject{
true: {admin, orgAdmin},
false: {member, orgMember, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "ReadTemplates",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceTemplate.InOrg(orgID).WithID(uuid.NewString()),
Assertions: map[bool][]authSubject{
true: {admin, orgMember, orgAdmin},
false: {member, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "Files",
Actions: []rbac.Action{rbac.ActionCreate},
Resource: rbac.ResourceFile,
Assertions: map[bool][]authSubject{
true: {admin},
false: {orgMember, orgAdmin, member, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "MyFile",
Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceFile.WithID(uuid.NewString()).WithOwner(meID.String()),
Assertions: map[bool][]authSubject{
true: {admin, member, orgMember},
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "CreateOrganizations",
Actions: []rbac.Action{rbac.ActionCreate},
Resource: rbac.ResourceOrganization,
Assertions: map[bool][]authSubject{
true: {admin},
false: {orgAdmin, otherOrgAdmin, otherOrgMember, member, orgMember},
},
},
{
Name: "Organizations",
Actions: []rbac.Action{rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceOrganization.InOrg(orgID).WithID(orgID.String()),
Assertions: map[bool][]authSubje 10000 ct{
true: {admin, orgAdmin},
false: {otherOrgAdmin, otherOrgMember, member, orgMember},
},
},
{
Name: "ReadOrganizations",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceOrganization.InOrg(orgID).WithID(orgID.String()),
Assertions: map[bool][]authSubject{
true: {admin, orgAdmin, orgMember},
false: {otherOrgAdmin, otherOrgMember, member},
},
},
{
Name: "RoleAssignment",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceRoleAssignment,
Assertions: map[bool][]authSubject{
true: {admin},
false: {orgAdmin, orgMember, otherOrgAdmin, otherOrgMember, member},
},
},
{
Name: "ReadRoleAssignment",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceRoleAssignment,
Assertions: map[bool][]authSubject{
true: {admin, orgAdmin, orgMember, otherOrgAdmin, otherOrgMember, member},
false: {},
},
},
{
Name: "OrgRoleAssignment",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
Assertions: map[bool][]authSubject{
true: {admin, orgAdmin},
false: {orgMember, otherOrgAdmin, otherOrgMember, member},
},
},
{
Name: "ReadOrgRoleAssignment",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
Assertions: map[bool][]authSubject{
true: {admin, orgAdmin, orgMember},
false: {otherOrgAdmin, otherOrgMember, member},
},
},
{
Name: "APIKey",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably clarify that the APIKey is owned by the user referred to by orgMember

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. I changed member and orgMember to memberMe and orgMemberMe respectively.

Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceAPIKey.WithOwner(meID.String()).WithID(uuid.NewString()),
Assertions: map[bool][]authSubject{
true: {admin, orgMember, member},
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "UserData",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceUserData.WithOwner(meID.String()).WithID(meID.String()),
Assertions: map[bool][]authSubject{
true: {admin, orgMember, member},
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "ManageOrgMember",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceOrganizationMember.InOrg(orgID).WithID(uuid.NewString()),
Assertions: map[bool][]authSubject{
true: {admin, orgAdmin},
false: {orgMember, member, otherOrgAdmin, otherOrgMember},
},
},
{
Name: "ReadOrgMember",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceOrganizationMember.InOrg(orgID).WithID(uuid.NewString()),
Assertions: map[bool][]authSubject{
true: {admin, orgAdmin, orgMember},
false: {member, otherOrgAdmin, otherOrgMember},
},
},
}

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.Assertions {
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()
Expand Down
0