mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
256 lines
7.8 KiB
Go
256 lines
7.8 KiB
Go
package dbauthz_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbmem"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
// TestInsertCustomRoles verifies creating custom roles cannot escalate permissions.
|
|
func TestInsertCustomRoles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
userID := uuid.New()
|
|
subjectFromRoles := func(roles rbac.ExpandableRoles) rbac.Subject {
|
|
return rbac.Subject{
|
|
FriendlyName: "Test user",
|
|
ID: userID.String(),
|
|
Roles: roles,
|
|
Groups: nil,
|
|
Scope: rbac.ScopeAll,
|
|
}
|
|
}
|
|
|
|
canCreateCustomRole := rbac.Role{
|
|
Identifier: rbac.RoleIdentifier{Name: "can-assign"},
|
|
DisplayName: "",
|
|
Site: rbac.Permissions(map[string][]policy.Action{
|
|
rbac.ResourceAssignRole.Type: {policy.ActionRead},
|
|
rbac.ResourceAssignOrgRole.Type: {policy.ActionRead, policy.ActionCreate},
|
|
}),
|
|
}
|
|
|
|
merge := func(u ...interface{}) rbac.Roles {
|
|
all := make([]rbac.Role, 0)
|
|
for _, v := range u {
|
|
v := v
|
|
switch t := v.(type) {
|
|
case rbac.Role:
|
|
all = append(all, t)
|
|
case rbac.ExpandableRoles:
|
|
all = append(all, must(t.Expand())...)
|
|
case rbac.RoleIdentifier:
|
|
all = append(all, must(rbac.RoleByName(t)))
|
|
default:
|
|
panic("unknown type")
|
|
}
|
|
}
|
|
|
|
return all
|
|
}
|
|
|
|
orgID := uuid.New()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
|
|
subject rbac.ExpandableRoles
|
|
|
|
// Perms to create on new custom role
|
|
organizationID uuid.UUID
|
|
site []codersdk.Permission
|
|
org []codersdk.Permission
|
|
user []codersdk.Permission
|
|
errorContains string
|
|
}{
|
|
{
|
|
// No roles, so no assign role
|
|
name: "no-roles",
|
|
organizationID: orgID,
|
|
subject: rbac.RoleIdentifiers{},
|
|
errorContains: "forbidden",
|
|
},
|
|
{
|
|
// This works because the new role has 0 perms
|
|
name: "empty",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole),
|
|
},
|
|
{
|
|
name: "mixed-scopes",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.RoleOwner()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
errorContains: "organization roles specify site or user permissions",
|
|
},
|
|
{
|
|
name: "invalid-action",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.RoleOwner()),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
// Action does not go with resource
|
|
codersdk.ResourceWorkspace: {codersdk.ActionViewInsights},
|
|
}),
|
|
errorContains: "invalid action",
|
|
},
|
|
{
|
|
name: "invalid-resource",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.RoleOwner()),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
"foobar": {codersdk.ActionViewInsights},
|
|
}),
|
|
errorContains: "invalid resource",
|
|
},
|
|
{
|
|
// Not allowing these at this time.
|
|
name: "negative-permission",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.RoleOwner()),
|
|
org: []codersdk.Permission{
|
|
{
|
|
Negate: true,
|
|
ResourceType: codersdk.ResourceWorkspace,
|
|
Action: codersdk.ActionRead,
|
|
},
|
|
},
|
|
errorContains: "no negative permissions",
|
|
},
|
|
{
|
|
name: "wildcard", // not allowed
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.RoleOwner()),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {"*"},
|
|
}),
|
|
errorContains: "no wildcard symbols",
|
|
},
|
|
// escalation checks
|
|
{
|
|
name: "read-workspace-escalation",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
errorContains: "not allowed to grant this permission",
|
|
},
|
|
{
|
|
name: "read-workspace-outside-org",
|
|
organizationID: uuid.New(),
|
|
subject: merge(canCreateCustomRole, rbac.ScopedRoleOrgAdmin(orgID)),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
errorContains: "not allowed to grant this permission",
|
|
},
|
|
{
|
|
name: "user-escalation",
|
|
// These roles do not grant user perms
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.ScopedRoleOrgAdmin(orgID)),
|
|
user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
errorContains: "organization roles specify site or user permissions",
|
|
},
|
|
{
|
|
name: "site-escalation",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.RoleTemplateAdmin()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceDeploymentConfig: {codersdk.ActionUpdate}, // not ok!
|
|
}),
|
|
errorContains: "organization roles specify site or user permissions",
|
|
},
|
|
// ok!
|
|
{
|
|
name: "read-workspace-template-admin",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.RoleTemplateAdmin()),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
},
|
|
{
|
|
name: "read-workspace-in-org",
|
|
organizationID: orgID,
|
|
subject: merge(canCreateCustomRole, rbac.ScopedRoleOrgAdmin(orgID)),
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
db := dbmem.New()
|
|
rec := &coderdtest.RecordingAuthorizer{
|
|
Wrapped: rbac.NewAuthorizer(prometheus.NewRegistry()),
|
|
}
|
|
az := dbauthz.New(db, rec, slog.Make(), coderdtest.AccessControlStorePointer())
|
|
|
|
subject := subjectFromRoles(tc.subject)
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
ctx = dbauthz.As(ctx, subject)
|
|
|
|
_, err := az.InsertCustomRole(ctx, database.InsertCustomRoleParams{
|
|
Name: "test-role",
|
|
DisplayName: "",
|
|
OrganizationID: uuid.NullUUID{UUID: tc.organizationID, Valid: true},
|
|
SitePermissions: db2sdk.List(tc.site, convertSDKPerm),
|
|
OrgPermissions: db2sdk.List(tc.org, convertSDKPerm),
|
|
UserPermissions: db2sdk.List(tc.user, convertSDKPerm),
|
|
})
|
|
if tc.errorContains != "" {
|
|
require.ErrorContains(t, err, tc.errorContains)
|
|
} else {
|
|
require.NoError(t, err)
|
|
|
|
// Verify the role is fetched with the lookup filter.
|
|
roles, err := az.CustomRoles(ctx, database.CustomRolesParams{
|
|
LookupRoles: []database.NameOrganizationPair{
|
|
{
|
|
Name: "test-role",
|
|
OrganizationID: tc.organizationID,
|
|
},
|
|
},
|
|
ExcludeOrgRoles: false,
|
|
OrganizationID: uuid.Nil,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, roles, 1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func convertSDKPerm(perm codersdk.Permission) database.CustomRolePermission {
|
|
return database.CustomRolePermission{
|
|
Negate: perm.Negate,
|
|
ResourceType: string(perm.ResourceType),
|
|
Action: policy.Action(perm.Action),
|
|
}
|
|
}
|