mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
5ccf5084e8
* chore: create type for unique role names Using `string` was confusing when something should be combined with org context, and when not to. Naming this new name, "RoleIdentifier"
272 lines
8.2 KiB
Go
272 lines
8.2 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"
|
|
)
|
|
|
|
// TestUpsertCustomRoles verifies creating custom roles cannot escalate permissions.
|
|
func TestUpsertCustomRoles(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,
|
|
}
|
|
}
|
|
|
|
canAssignRole := rbac.Role{
|
|
Identifier: rbac.RoleIdentifier{Name: "can-assign"},
|
|
DisplayName: "",
|
|
Site: rbac.Permissions(map[string][]policy.Action{
|
|
rbac.ResourceAssignRole.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.NullUUID{
|
|
UUID: uuid.New(),
|
|
Valid: true,
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
|
|
subject rbac.ExpandableRoles
|
|
|
|
// Perms to create on new custom role
|
|
organizationID uuid.NullUUID
|
|
site []codersdk.Permission
|
|
org []codersdk.Permission
|
|
user []codersdk.Permission
|
|
errorContains string
|
|
}{
|
|
{
|
|
// No roles, so no assign role
|
|
name: "no-roles",
|
|
subject: rbac.RoleIdentifiers{},
|
|
errorContains: "forbidden",
|
|
},
|
|
{
|
|
// This works because the new role has 0 perms
|
|
name: "empty",
|
|
subject: merge(canAssignRole),
|
|
},
|
|
{
|
|
name: "mixed-scopes",
|
|
subject: merge(canAssignRole, rbac.RoleOwner()),
|
|
organizationID: orgID,
|
|
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: "cannot assign both org and site permissions",
|
|
},
|
|
{
|
|
name: "invalid-action",
|
|
subject: merge(canAssignRole, rbac.RoleOwner()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
// Action does not go with resource
|
|
codersdk.ResourceWorkspace: {codersdk.ActionViewInsights},
|
|
}),
|
|
errorContains: "invalid action",
|
|
},
|
|
{
|
|
name: "invalid-resource",
|
|
subject: merge(canAssignRole, rbac.RoleOwner()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
"foobar": {codersdk.ActionViewInsights},
|
|
}),
|
|
errorContains: "invalid resource",
|
|
},
|
|
{
|
|
// Not allowing these at this time.
|
|
name: "negative-permission",
|
|
subject: merge(canAssignRole, rbac.RoleOwner()),
|
|
site: []codersdk.Permission{
|
|
{
|
|
Negate: true,
|
|
ResourceType: codersdk.ResourceWorkspace,
|
|
Action: codersdk.ActionRead,
|
|
},
|
|
},
|
|
errorContains: "no negative permissions",
|
|
},
|
|
{
|
|
name: "wildcard", // not allowed
|
|
subject: merge(canAssignRole, rbac.RoleOwner()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {"*"},
|
|
}),
|
|
errorContains: "no wildcard symbols",
|
|
},
|
|
// escalation checks
|
|
{
|
|
name: "read-workspace-escalation",
|
|
subject: merge(canAssignRole),
|
|
site: 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.NullUUID{
|
|
UUID: uuid.New(),
|
|
Valid: true,
|
|
},
|
|
subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)),
|
|
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
|
|
subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)),
|
|
user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
errorContains: "not allowed to grant this permission",
|
|
},
|
|
{
|
|
name: "template-admin-escalation",
|
|
subject: merge(canAssignRole, rbac.RoleTemplateAdmin()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead}, // ok!
|
|
codersdk.ResourceDeploymentConfig: {codersdk.ActionUpdate}, // not ok!
|
|
}),
|
|
user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead}, // ok!
|
|
}),
|
|
errorContains: "deployment_config",
|
|
},
|
|
// ok!
|
|
{
|
|
name: "read-workspace-template-admin",
|
|
subject: merge(canAssignRole, rbac.RoleTemplateAdmin()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
},
|
|
{
|
|
name: "read-workspace-in-org",
|
|
subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)),
|
|
organizationID: orgID,
|
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
},
|
|
{
|
|
name: "user-perms",
|
|
// This is weird, but is ok
|
|
subject: merge(canAssignRole, rbac.RoleMember()),
|
|
user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
},
|
|
{
|
|
name: "site+user-perms",
|
|
subject: merge(canAssignRole, rbac.RoleMember(), rbac.RoleTemplateAdmin()),
|
|
site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
|
}),
|
|
user: 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.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
|
Name: "test-role",
|
|
DisplayName: "",
|
|
OrganizationID: tc.organizationID,
|
|
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.UUID,
|
|
},
|
|
},
|
|
ExcludeOrgRoles: false,
|
|
OrganizationID: uuid.UUID{},
|
|
})
|
|
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),
|
|
}
|
|
}
|