From ebaae7599355ac70a6329913bd3b205ffa443bc4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 27 May 2022 08:48:19 -0500 Subject: [PATCH] test: Unit test to assert role capabilities (#1781) * test: Unit test to assert role permissions This unit test allows for asserting which roles can perform actions on various objects. This is much easier than making unit tests to hit the api. --- coderd/rbac/builtin_test.go | 242 +++++++++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 1 deletion(-) diff --git a/coderd/rbac/builtin_test.go b/coderd/rbac/builtin_test.go index 44f54a3626..d68f697719 100644 --- a/coderd/rbac/builtin_test.go +++ b/coderd/rbac/builtin_test.go @@ -1,16 +1,256 @@ 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") + + // currentUser is anything that references "me", "mine", or "my". + currentUser := uuid.New() + adminID := 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)}} + + admin := authSubject{Name: "admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleAdmin()}} + 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)}} + + // requiredSubjects are required to be asserted in each test case. This is + // to make sure one is not forgotten. + requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember} + + 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.WithID(currentUser.String()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin}, + false: {}, + }, + }, + { + Name: "AUser", + Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceUser, + AuthorizeMap: map[bool][]authSubject{ + true: {admin}, + false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin}, + }, + }, + { + 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()).WithID(uuid.NewString()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgMemberMe, orgAdmin}, + false: {memberMe, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "Templates", + Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceTemplate.InOrg(orgID).WithID(uuid.NewString()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin}, + false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "ReadTemplates", + Actions: []rbac.Action{rbac.ActionRead}, + Resource: rbac.ResourceTemplate.InOrg(orgID).WithID(uuid.NewString()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgMemberMe, orgAdmin}, + false: {memberMe, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "Files", + Actions: []rbac.Action{rbac.ActionCreate}, + Resource: rbac.ResourceFile, + AuthorizeMap: map[bool][]authSubject{ + true: {admin}, + false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "MyFile", + Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceFile.WithID(uuid.NewString()).WithOwner(currentUser.String()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, memberMe, orgMemberMe}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "CreateOrganizations", + Actions: []rbac.Action{rbac.ActionCreate}, + Resource: rbac.ResourceOrganization, + AuthorizeMap: map[bool][]authSubject{ + true: {admin}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe}, + }, + }, + { + Name: "Organizations", + Actions: []rbac.Action{rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceOrganization.InOrg(orgID).WithID(orgID.String()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin}, + false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe}, + }, + }, + { + Name: "ReadOrganizations", + Actions: []rbac.Action{rbac.ActionRead}, + Resource: rbac.ResourceOrganization.InOrg(orgID).WithID(orgID.String()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin, orgMemberMe}, + false: {otherOrgAdmin, otherOrgMember, memberMe}, + }, + }, + { + Name: "RoleAssignment", + Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceRoleAssignment, + AuthorizeMap: map[bool][]authSubject{ + true: {admin}, + false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe}, + }, + }, + { + Name: "ReadRoleAssignment", + Actions: []rbac.Action{rbac.ActionRead}, + Resource: rbac.ResourceRoleAssignment, + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe}, + false: {}, + }, + }, + { + Name: "OrgRoleAssignment", + Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin}, + false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe}, + }, + }, + { + Name: "ReadOrgRoleAssignment", + Actions: []rbac.Action{rbac.ActionRead}, + Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin, orgMemberMe}, + false: {otherOrgAdmin, otherOrgMember, memberMe}, + }, + }, + { + Name: "APIKey", + Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceAPIKey.WithOwner(currentUser.String()).WithID(uuid.NewString()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgMemberMe, memberMe}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "UserData", + Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceUserData.WithOwner(currentUser.String()).WithID(currentUser.String()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgMemberMe, memberMe}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "ManageOrgMember", + Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, + Resource: rbac.ResourceOrganizationMember.InOrg(orgID).WithID(uuid.NewString()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin}, + false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember}, + }, + }, + { + Name: "ReadOrgMember", + Actions: []rbac.Action{rbac.ActionRead}, + Resource: rbac.ResourceOrganizationMember.InOrg(orgID).WithID(uuid.NewString()), + AuthorizeMap: map[bool][]authSubject{ + true: {admin, orgAdmin, orgMemberMe}, + false: {memberMe, 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.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()