feat: Option to remove WorkspaceExec from `owner` role (#7050)

* chore: Add AllResources option for listing all RBAC objects
* Owners cannot do workspace exec site wide
* Fix FE authchecks to valid RBAC resources
This commit is contained in:
Steven Masley 2023-04-11 08:57:23 -05:00 committed by GitHub
parent ad2353c3d8
commit 9d39371ee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 700 additions and 169 deletions

View File

@ -423,6 +423,7 @@ gen: \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
docs/admin/prometheus.md \
docs/cli.md \
docs/admin/audit-logs.md \
@ -443,6 +444,7 @@ gen/mark-fresh:
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
docs/admin/prometheus.md \
docs/cli.md \
docs/admin/audit-logs.md \
@ -495,6 +497,9 @@ site/src/api/typesGenerated.ts: scripts/apitypings/main.go $(shell find ./coders
cd site
yarn run format:types
coderd/rbac/object_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go
go run scripts/rbacgen/main.go ./coderd/rbac > coderd/rbac/object_gen.go
docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
go run scripts/metricsdocgen/main.go
cd site
@ -505,12 +510,12 @@ docs/cli.md: scripts/clidocgen/main.go $(GO_SRC_FILES) docs/manifest.json
cd site
yarn run format:write:only ../docs/cli.md ../docs/cli/*.md ../docs/manifest.json
docs/admin/audit-logs.md: scripts/auditdocgen/main.go enterprise/audit/table.go
docs/admin/audit-logs.md: scripts/auditdocgen/main.go enterprise/audit/table.go coderd/rbac/object_gen.go
go run scripts/auditdocgen/main.go
cd site
yarn run format:write:only ../docs/admin/audit-logs.md
coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS) -type f) $(wildcard coderd/*.go) $(wildcard enterprise/coderd/*.go) $(wildcard codersdk/*.go) .swaggo docs/manifest.json
coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS) -type f) $(wildcard coderd/*.go) $(wildcard enterprise/coderd/*.go) $(wildcard codersdk/*.go) .swaggo docs/manifest.json coderd/rbac/object_gen.go
./scripts/apidocgen/generate.sh
yarn run --cwd=site format:write:only ../docs/api ../docs/manifest.json ../coderd/apidoc/swagger.json

View File

@ -16,6 +16,12 @@ Start a Coder server
$CACHE_DIRECTORY is set, it will be used for compatibility with
systemd.
--disable-owner-workspace-access bool, $CODER_DISABLE_OWNER_WORKSPACE_ACCESS
Remove the permission for the 'owner' role to have workspace execution
on all workspaces. This prevents the 'owner' from ssh, apps, and
terminal access based on the 'owner' role. They still have their user
permissions to access their own workspaces.
--disable-path-apps bool, $CODER_DISABLE_PATH_APPS
Disable workspace apps that are not served from subdomains. Path-based
apps can make requests to the Coder API and pose a security risk when

View File

@ -315,6 +315,12 @@ agentFallbackTroubleshootingURL: https://coder.com/docs/coder-oss/latest/templat
# --wildcard-access-url is configured.
# (default: <unset>, type: bool)
disablePathApps: false
# Remove the permission for the 'owner' role to have workspace execution on all
# workspaces. This prevents the 'owner' from ssh, apps, and terminal access based
# on the 'owner' role. They still have their user permissions to access their own
# workspaces.
# (default: <unset>, type: bool)
disableOwnerWorkspaceAccess: false
# These options change the behavior of how clients interact with the Coder.
# Clients include the coder cli, vs code extension, and the web UI.
client:

60
coderd/apidoc/docs.go generated
View File

@ -6290,7 +6290,11 @@ const docTemplate = `{
},
"resource_type": {
"description": "ResourceType is the name of the resource.\n` + "`" + `./coderd/rbac/object.go` + "`" + ` has the list of valid resource types.",
"type": "string"
"allOf": [
{
"$ref": "#/definitions/codersdk.RBACResource"
}
]
}
}
},
@ -6985,6 +6989,9 @@ const docTemplate = `{
"derp": {
"$ref": "#/definitions/codersdk.DERP"
},
"disable_owner_workspace_exec": {
"type": "boolean"
},
"disable_password_auth": {
"type": "boolean"
},
@ -8023,6 +8030,57 @@ const docTemplate = `{
}
}
},
"codersdk.RBACResource": {
"type": "string",
"enum": [
"workspace",
"workspace_proxy",
"workspace_execution",
"application_connect",
"audit_log",
"template",
"group",
"file",
"provisioner_daemon",
"organization",
"assign_role",
"assign_org_role",
"api_key",
"user",
"user_data",
"organization_member",
"license",
"deployment_config",
"deployment_stats",
"replicas",
"debug_info",
"system"
],
"x-enum-varnames": [
"ResourceWorkspace",
"ResourceWorkspaceProxy",
"ResourceWorkspaceExecution",
"ResourceWorkspaceApplicationConnect",
"ResourceAuditLog",
"ResourceTemplate",
"ResourceGroup",
"ResourceFile",
"ResourceProvisionerDaemon",
"ResourceOrganization",
"ResourceRoleAssignment",
"ResourceOrgRoleAssignment",
"ResourceAPIKey",
"ResourceUser",
"ResourceUserData",
"ResourceOrganizationMember",
"ResourceLicense",
"ResourceDeploymentValues",
"ResourceDeploymentStats",
"ResourceReplicas",
"ResourceDebugInfo",
"ResourceSystem"
]
},
"codersdk.RateLimitConfig": {
"type": "object",
"properties": {

View File

@ -5600,7 +5600,11 @@
},
"resource_type": {
"description": "ResourceType is the name of the resource.\n`./coderd/rbac/object.go` has the list of valid resource types.",
"type": "string"
"allOf": [
{
"$ref": "#/definitions/codersdk.RBACResource"
}
]
}
}
},
@ -6237,6 +6241,9 @@
"derp": {
"$ref": "#/definitions/codersdk.DERP"
},
"disable_owner_workspace_exec": {
"type": "boolean"
},
"disable_password_auth": {
"type": "boolean"
},
@ -7182,6 +7189,57 @@
}
}
},
"codersdk.RBACResource": {
"type": "string",
"enum": [
"workspace",
"workspace_proxy",
"workspace_execution",
"application_connect",
"audit_log",
"template",
"group",
"file",
"provisioner_daemon",
"organization",
"assign_role",
"assign_org_role",
"api_key",
"user",
"user_data",
"organization_member",
"license",
"deployment_config",
"deployment_stats",
"replicas",
"debug_info",
"system"
],
"x-enum-varnames": [
"ResourceWorkspace",
"ResourceWorkspaceProxy",
"ResourceWorkspaceExecution",
"ResourceWorkspaceApplicationConnect",
"ResourceAuditLog",
"ResourceTemplate",
"ResourceGroup",
"ResourceFile",
"ResourceProvisionerDaemon",
"ResourceOrganization",
"ResourceRoleAssignment",
"ResourceOrgRoleAssignment",
"ResourceAPIKey",
"ResourceUser",
"ResourceUserData",
"ResourceOrganizationMember",
"ResourceLicense",
"ResourceDeploymentValues",
"ResourceDeploymentStats",
"ResourceReplicas",
"ResourceDebugInfo",
"ResourceSystem"
]
},
"codersdk.RateLimitConfig": {
"type": "object",
"properties": {

View File

@ -168,7 +168,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
obj := rbac.Object{
Owner: v.Object.OwnerID,
OrgID: v.Object.OrganizationID,
Type: v.Object.ResourceType,
Type: v.Object.ResourceType.String(),
}
if obj.Owner == "me" {
obj.Owner = auth.Actor.ID
@ -188,7 +188,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
var dbObj rbac.Objecter
var dbErr error
// Only support referencing some resources by ID.
switch v.Object.ResourceType {
switch v.Object.ResourceType.String() {
case rbac.ResourceWorkspaceExecution.Type:
wrkSpace, err := api.Database.GetWorkspaceByID(ctx, id)
if err == nil {

View File

@ -46,34 +46,34 @@ func TestCheckPermissions(t *testing.T) {
params := map[string]codersdk.AuthorizationCheck{
readAllUsers: {
Object: codersdk.AuthorizationObject{
ResourceType: "users",
ResourceType: codersdk.ResourceUser,
},
Action: "read",
},
readMyself: {
Object: codersdk.AuthorizationObject{
ResourceType: "users",
ResourceType: codersdk.ResourceUser,
OwnerID: "me",
},
Action: "read",
},
readOwnWorkspaces: {
Object: codersdk.AuthorizationObject{
ResourceType: "workspaces",
ResourceType: codersdk.ResourceWorkspace,
OwnerID: "me",
},
Action: "read",
},
readOrgWorkspaces: {
Object: codersdk.AuthorizationObject{
ResourceType: "workspaces",
ResourceType: codersdk.ResourceWorkspace,
OrganizationID: adminUser.OrganizationID.String(),
},
Action: "read",
},
updateSpecificTemplate: {
Object: codersdk.AuthorizationObject{
ResourceType: rbac.ResourceTemplate.Type,
ResourceType: codersdk.ResourceTemplate,
ResourceID: template.ID.String(),
},
Action: "update",
@ -103,7 +103,7 @@ func TestCheckPermissions(t *testing.T) {
Client: orgAdminClient,
UserID: orgAdminUser.ID,
Check: map[string]bool{
readAllUsers: false,
readAllUsers: true,
readMyself: true,
readOwnWorkspaces: true,
readOrgWorkspaces: true,
@ -115,7 +115,7 @@ func TestCheckPermissions(t *testing.T) {
Client: memberClient,
UserID: memberUser.ID,
Check: map[string]bool{
readAllUsers: false,
readAllUsers: true,
readMyself: true,
readOwnWorkspaces: true,
readOrgWorkspaces: false,

View File

@ -171,6 +171,12 @@ func New(options *Options) *API {
options = &Options{}
}
if options.DeploymentValues.DisableOwnerWorkspaceExec {
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoOwnerWorkspaceExec: true,
})
}
if options.Authorizer == nil {
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
}

View File

@ -203,6 +203,8 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
if options.DeploymentValues == nil {
options.DeploymentValues = DeploymentValues(t)
}
// This value is not safe to run in parallel. Force it to be false.
options.DeploymentValues.DisableOwnerWorkspaceExec = false
// If no ratelimits are set, disable all rate limiting for tests.
if options.APIRateLimit == 0 {

View File

@ -14,6 +14,12 @@ type Objecter interface {
// Resources are just typed objects. Making resources this way allows directly
// passing them into an Authorize function and use the chaining api.
var (
// ResourceWildcard represents all resource types
// Try to avoid using this where possible.
ResourceWildcard = Object{
Type: WildcardSymbol,
}
// ResourceWorkspace CRUD. Org + User owner
// create/delete = make or delete workspaces
// read = access workspace
@ -136,11 +142,6 @@ var (
Type: "organization_member",
}
// ResourceWildcard represents all resource types
ResourceWildcard = Object{
Type: WildcardSymbol,
}
// ResourceLicense is the license in the 'licenses' table.
// ResourceLicense is site wide.
// create/delete = add or remove license from site.

30
coderd/rbac/object_gen.go Normal file
View File

@ -0,0 +1,30 @@
// Code generated by rbacgen/main.go. DO NOT EDIT.
package rbac
func AllResources() []Object {
return []Object{
ResourceAPIKey,
ResourceAuditLog,
ResourceDebugInfo,
ResourceDeploymentStats,
ResourceDeploymentValues,
ResourceFile,
ResourceGroup,
ResourceLicense,
ResourceOrgRoleAssignment,
ResourceOrganization,
ResourceOrganizationMember,
ResourceProvisionerDaemon,
ResourceReplicas,
ResourceRoleAssignment,
ResourceSystem,
ResourceTemplate,
ResourceUser,
ResourceUserData,
ResourceWildcard,
ResourceWorkspace,
ResourceWorkspaceApplicationConnect,
ResourceWorkspaceExecution,
ResourceWorkspaceProxy,
}
}

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/util/slice"
)
func TestObjectEqual(t *testing.T) {
@ -174,3 +175,22 @@ func TestObjectEqual(t *testing.T) {
})
}
}
// TestAllResources ensures that all resources have a unique type name.
func TestAllResources(t *testing.T) {
t.Parallel()
var typeNames []string
resources := rbac.AllResources()
for _, r := range resources {
if r.Type == "" {
t.Errorf("empty type name: %s", r.Type)
continue
}
if slice.Contains(typeNames, r.Type) {
t.Errorf("duplicate type name: %s", r.Type)
continue
}
typeNames = append(typeNames, r.Type)
}
}

View File

@ -20,6 +20,11 @@ const (
orgMember string = "organization-member"
)
func init() {
// Always load defaults
ReloadBuiltinRoles(nil)
}
// RoleNames is a list of user assignable role names. The role names must be
// in the builtInRoles map. Any non-user assignable roles will generate an
// error on Expand.
@ -62,6 +67,33 @@ func RoleOrgMember(organizationID uuid.UUID) string {
return roleName(orgMember, organizationID.String())
}
func allPermsExcept(excepts ...Object) []Permission {
resources := AllResources()
var perms []Permission
skip := make(map[string]bool)
for _, e := range excepts {
skip[e.Type] = true
}
for _, r := range resources {
// Exceptions
if skip[r.Type] {
continue
}
// This should always be skipped.
if r.Type == ResourceWildcard.Type {
continue
}
// Owners can do everything else
perms = append(perms, Permission{
Negate: false,
ResourceType: r.Type,
Action: WildcardSymbol,
})
}
return perms
}
// builtInRoles are just a hard coded set for now. Ideally we store these in
// the database. Right now they are functions because the org id should scope
// certain roles. When we store them in the database, each organization should
@ -70,145 +102,163 @@ func RoleOrgMember(organizationID uuid.UUID) string {
//
// This map will be replaced by database storage defined by this ticket.
// https://github.com/coder/coder/issues/1194
var builtInRoles = map[string]func(orgID string) Role{
// admin grants all actions to all resources.
owner: func(_ string) Role {
return Role{
Name: owner,
DisplayName: "Owner",
Site: Permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
Org: map[string][]Permission{},
User: []Permission{},
}
},
var builtInRoles map[string]func(orgID string) Role
// member grants all actions to all resources owned by the user
member: func(_ string) Role {
return Role{
Name: member,
DisplayName: "",
Site: Permissions(map[string][]Action{
// All users can read all other users and know they exist.
ResourceUser.Type: {ActionRead},
ResourceRoleAssignment.Type: {ActionRead},
// All users can see the provisioner daemons.
ResourceProvisionerDaemon.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: Permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
}
},
type RoleOptions struct {
NoOwnerWorkspaceExec bool
}
// auditor provides all permissions required to effectively read and understand
// audit log events.
// TODO: Finish the auditor as we add resources.
auditor: func(_ string) Role {
return Role{
Name: auditor,
DisplayName: "Auditor",
Site: Permissions(map[string][]Action{
// Should be able to read all template details, even in orgs they
// are not in.
ResourceTemplate.Type: {ActionRead},
ResourceAuditLog.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
}
},
// ReloadBuiltinRoles loads the static roles into the builtInRoles map.
// This can be called again with a different config to change the behavior.
//
// TODO: @emyrk This would be great if it was instanced to a coderd rather
// than a global. But that is a much larger refactor right now.
// Essentially we did not foresee different deployments needing slightly
// different role permissions.
func ReloadBuiltinRoles(opts *RoleOptions) {
if opts == nil {
opts = &RoleOptions{}
}
templateAdmin: func(_ string) Role {
return Role{
Name: templateAdmin,
DisplayName: "Template Admin",
Site: Permissions(map[string][]Action{
ResourceTemplate.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// CRUD all files, even those they did not upload.
ResourceFile.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceWorkspace.Type: {ActionRead},
// CRUD to provisioner daemons for now.
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// Needs to read all organizations since
ResourceOrganization.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
}
},
var ownerAndAdminExceptions []Object
if opts.NoOwnerWorkspaceExec {
ownerAndAdminExceptions = append(ownerAndAdminExceptions,
ResourceWorkspaceExecution,
ResourceWorkspaceApplicationConnect,
)
}
userAdmin: func(_ string) Role {
return Role{
Name: userAdmin,
DisplayName: "User Admin",
Site: Permissions(map[string][]Action{
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// Full perms to manage org members
ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceGroup.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
}),
Org: map[string][]Permission{},
User: []Permission{},
}
},
builtInRoles = map[string]func(orgID string) Role{
// admin grants all actions to all resources.
owner: func(_ string) Role {
return Role{
Name: owner,
DisplayName: "Owner",
Site: allPermsExcept(ownerAndAdminExceptions...),
Org: map[string][]Permission{},
User: []Permission{},
}
},
// orgAdmin returns a role with all actions allows in a given
// organization scope.
orgAdmin: func(organizationID string) Role {
return Role{
Name: roleName(orgAdmin, organizationID),
DisplayName: "Organization Admin",
Site: []Permission{},
Org: map[string][]Permission{
organizationID: {
{
Negate: false,
ResourceType: "*",
Action: "*",
// member grants all actions to all resources owned by the user
member: func(_ string) Role {
return Role{
Name: member,
DisplayName: "",
Site: Permissions(map[string][]Action{
// All users can read all other users and know they exist.
ResourceUser.Type: {ActionRead},
ResourceRoleAssignment.Type: {ActionRead},
// All users can see the provisioner daemons.
ResourceProvisionerDaemon.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: allPermsExcept(),
}
},
// auditor provides all permissions required to effectively read and understand
// audit log events.
// TODO: Finish the auditor as we add resources.
auditor: func(_ string) Role {
return Role{
Name: auditor,
DisplayName: "Auditor",
Site: Permissions(map[string][]Action{
// Should be able to read all template details, even in orgs they
// are not in.
ResourceTemplate.Type: {ActionRead},
ResourceAuditLog.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
}
},
templateAdmin: func(_ string) Role {
return Role{
Name: templateAdmin,
DisplayName: "Template Admin",
Site: Permissions(map[string][]Action{
ResourceTemplate.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// CRUD all files, even those they did not upload.
ResourceFile.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceWorkspace.Type: {ActionRead},
// CRUD to provisioner daemons for now.
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// Needs to read all organizations since
ResourceOrganization.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
}
},
userAdmin: func(_ string) Role {
return Role{
Name: userAdmin,
DisplayName: "User Admin",
Site: Permissions(map[string][]Action{
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// Full perms to manage org members
ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceGroup.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
}),
Org: map[string][]Permission{},
User: []Permission{},
}
},
// orgAdmin returns a role with all actions allows in a given
// organization scope.
orgAdmin: func(organizationID string) Role {
return Role{
Name: roleName(orgAdmin, organizationID),
DisplayName: "Organization Admin",
Site: []Permission{},
Org: map[string][]Permission{
// Org admins should not have workspace exec perms.
organizationID: allPermsExcept(ResourceWorkspaceExecution),
},
User: []Permission{},
}
},
// orgMember has an empty set of permissions, this just implies their membership
// in an organization.
orgMember: func(organizationID string) Role {
return Role{
Name: roleName(orgMember, organizationID),
DisplayName: "",
Site: []Permission{},
Org: map[string][]Permission{
organizationID: {
{
// All org members can read the other members in their org.
ResourceType: ResourceOrganizationMember.Type,
Action: ActionRead,
},
{
// All org members can read the organization
ResourceType: ResourceOrganization.Type,
Action: ActionRead,
},
{
// Can read available roles.
ResourceType: ResourceOrgRoleAssignment.Type,
Action: ActionRead,
},
{
ResourceType: ResourceGroup.Type,
Action: ActionRead,
},
},
},
},
User: []Permission{},
}
},
// orgMember has an empty set of permissions, this just implies their membership
// in an organization.
orgMember: func(organizationID string) Role {
return Role{
Name: roleName(orgMember, organizationID),
DisplayName: "",
Site: []Permission{},
Org: map[string][]Permission{
organizationID: {
{
// All org members can read the other members in their org.
ResourceType: ResourceOrganizationMember.Type,
Action: ActionRead,
},
{
// All org members can read the organization
ResourceType: ResourceOrganization.Type,
Action: ActionRead,
},
{
// Can read available roles.
ResourceType: ResourceOrgRoleAssignment.Type,
Action: ActionRead,
},
{
ResourceType: ResourceGroup.Type,
Action: ActionRead,
},
},
},
User: []Permission{},
}
},
User: []Permission{},
}
},
}
}
// assignRoles is a map of roles that can be assigned if a user has a given

View File

@ -19,6 +19,42 @@ type authSubject struct {
Actor rbac.Subject
}
//nolint:tparallel,paralleltest
func TestOwnerExec(t *testing.T) {
owner := rbac.Subject{
ID: uuid.NewString(),
Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleOwner()},
Scope: rbac.ScopeAll,
}
t.Run("NoExec", func(t *testing.T) {
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoOwnerWorkspaceExec: true,
})
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
// Exec a random workspace
err := auth.Authorize(context.Background(), owner, rbac.ActionCreate,
rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
require.ErrorAsf(t, err, &rbac.UnauthorizedError{}, "expected unauthorized error")
})
t.Run("Exec", func(t *testing.T) {
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoOwnerWorkspaceExec: false,
})
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
// Exec a random workspace
err := auth.Authorize(context.Background(), owner, rbac.ActionCreate,
rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
require.NoError(t, err, "expected owner can")
})
}
// TODO: add the SYSTEM to the MATRIX
func TestRolePermissions(t *testing.T) {
t.Parallel()
@ -111,8 +147,8 @@ func TestRolePermissions(t *testing.T) {
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceWorkspaceExecution.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, orgMemberMe},
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
true: {owner, orgMemberMe},
false: {orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
},
},
{

View File

@ -43,7 +43,7 @@ type AuthorizationCheck struct {
type AuthorizationObject struct {
// ResourceType is the name of the resource.
// `./coderd/rbac/object.go` has the list of valid resource types.
ResourceType string `json:"resource_type"`
ResourceType RBACResource `json:"resource_type"`
// OwnerID (optional) adds the set constraint to all resources owned by a given user.
OwnerID string `json:"owner_id,omitempty"`
// OrganizationID (optional) adds the set constraint to all resources owned by a given organization.

View File

@ -162,6 +162,7 @@ type DeploymentValues struct {
GitAuthProviders clibase.Struct[[]GitAuthConfig] `json:"git_auth,omitempty" typescript:",notnull"`
SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"`
WgtunnelHost clibase.String `json:"wgtunnel_host,omitempty" typescript:",notnull"`
DisableOwnerWorkspaceExec clibase.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"`
Config clibase.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
WriteConfig clibase.Bool `json:"write_config,omitempty" typescript:",notnull"`
@ -1320,6 +1321,15 @@ when required by your organization's security policy.`,
Value: &c.DisablePathApps,
YAML: "disablePathApps",
},
{
Name: "Disable Owner Workspace Access",
Description: "Remove the permission for the 'owner' role to have workspace execution on all workspaces. This prevents the 'owner' from ssh, apps, and terminal access based on the 'owner' role. They still have their user permissions to access their own workspaces.",
Flag: "disable-owner-workspace-access",
Env: "CODER_DISABLE_OWNER_WORKSPACE_ACCESS",
Value: &c.DisableOwnerWorkspaceExec,
YAML: "disableOwnerWorkspaceAccess",
},
{
Name: "Session Duration",
Description: "The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh.",

32
codersdk/rbacresources.go Normal file
View File

@ -0,0 +1,32 @@
package codersdk
type RBACResource string
const (
ResourceWorkspace RBACResource = "workspace"
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
ResourceWorkspaceExecution RBACResource = "workspace_execution"
ResourceWorkspaceApplicationConnect RBACResource = "application_connect"
ResourceAuditLog RBACResource = "audit_log"
ResourceTemplate RBACResource = "template"
ResourceGroup RBACResource = "group"
ResourceFile RBACResource = "file"
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
ResourceOrganization RBACResource = "organization"
ResourceRoleAssignment RBACResource = "assign_role"
ResourceOrgRoleAssignment RBACResource = "assign_org_role"
ResourceAPIKey RBACResource = "api_key"
ResourceUser RBACResource = "user"
ResourceUserData RBACResource = "user_data"
ResourceOrganizationMember RBACResource = "organization_member"
ResourceLicense RBACResource = "license"
ResourceDeploymentValues RBACResource = "deployment_config"
ResourceDeploymentStats RBACResource = "deployment_stats"
ResourceReplicas RBACResource = "replicas"
ResourceDebugInfo RBACResource = "debug_info"
ResourceSystem RBACResource = "system"
)
func (r RBACResource) String() string {
return string(r)
}

View File

@ -25,7 +25,7 @@ curl -X POST http://coder-server:8080/api/v2/authcheck \
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "string"
"resource_type": "workspace"
}
},
"property2": {
@ -34,7 +34,7 @@ curl -X POST http://coder-server:8080/api/v2/authcheck \
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "string"
"resource_type": "workspace"
}
}
}

View File

@ -188,6 +188,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"stun_addresses": ["string"]
}
},
"disable_owner_workspace_exec": true,
"disable_password_auth": true,
"disable_path_apps": true,
"disable_session_expiry_refresh": true,

View File

@ -1042,7 +1042,7 @@
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "string"
"resource_type": "workspace"
}
}
```
@ -1072,7 +1072,7 @@ AuthorizationCheck is used to check if the currently authenticated user (or the
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "string"
"resource_type": "workspace"
}
```
@ -1080,12 +1080,12 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
| Name | Type | Required | Restrictions | Description |
| ----------------- | ------ | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `organization_id` | string | false | | Organization ID (optional) adds the set constraint to all resources owned by a given organization. |
| `owner_id` | string | false | | Owner ID (optional) adds the set constraint to all resources owned by a given user. |
| `resource_id` | string | false | | Resource ID (optional) reduces the set to a singular resource. This assigns a resource ID to the resource type, eg: a single workspace. The rbac library will not fetch the resource from the database, so if you are using this option, you should also set the owner ID and organization ID if possible. Be as specific as possible using all the fields relevant. |
| `resource_type` | string | false | | Resource type is the name of the resource. `./coderd/rbac/object.go` has the list of valid resource types. |
| Name | Type | Required | Restrictions | Description |
| ----------------- | ---------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `organization_id` | string | false | | Organization ID (optional) adds the set constraint to all resources owned by a given organization. |
| `owner_id` | string | false | | Owner ID (optional) adds the set constraint to all resources owned by a given user. |
| `resource_id` | string | false | | Resource ID (optional) reduces the set to a singular resource. This assigns a resource ID to the resource type, eg: a single workspace. The rbac library will not fetch the resource from the database, so if you are using this option, you should also set the owner ID and organization ID if possible. Be as specific as possible using all the fields relevant. |
| `resource_type` | [codersdk.RBACResource](#codersdkrbacresource) | false | | Resource type is the name of the resource. `./coderd/rbac/object.go` has the list of valid resource types. |
## codersdk.AuthorizationRequest
@ -1098,7 +1098,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "string"
"resource_type": "workspace"
}
},
"property2": {
@ -1107,7 +1107,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "string"
"resource_type": "workspace"
}
}
}
@ -1817,6 +1817,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
"stun_addresses": ["string"]
}
},
"disable_owner_workspace_exec": true,
"disable_password_auth": true,
"disable_path_apps": true,
"disable_session_expiry_refresh": true,
@ -2159,6 +2160,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
"stun_addresses": ["string"]
}
},
"disable_owner_workspace_exec": true,
"disable_password_auth": true,
"disable_path_apps": true,
"disable_session_expiry_refresh": true,
@ -2347,6 +2349,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
| `config_ssh` | [codersdk.SSHConfig](#codersdksshconfig) | false | | |
| `dangerous` | [codersdk.DangerousConfig](#codersdkdangerousconfig) | false | | |
| `derp` | [codersdk.DERP](#codersdkderp) | false | | |
| `disable_owner_workspace_exec` | boolean | false | | |
| `disable_password_auth` | boolean | false | | |
| `disable_path_apps` | boolean | false | | |
| `disable_session_expiry_refresh` | boolean | false | | |
@ -3359,6 +3362,41 @@ Parameter represents a set value for the scope.
| ---------- | ------ | -------- | ------------ | ----------- |
| `deadline` | string | true | | |
## codersdk.RBACResource
```json
"workspace"
```
### Properties
#### Enumerated Values
| Value |
| --------------------- |
| `workspace` |
| `workspace_proxy` |
| `workspace_execution` |
| `application_connect` |
| `audit_log` |
| `template` |
| `group` |
| `file` |
| `provisioner_daemon` |
| `organization` |
| `assign_role` |
| `assign_org_role` |
| `api_key` |
| `user` |
| `user_data` |
| `organization_member` |
| `license` |
| `deployment_config` |
| `deployment_stats` |
| `replicas` |
| `debug_info` |
| `system` |
## codersdk.RateLimitConfig
```json

View File

@ -173,6 +173,16 @@ An HTTP URL that is accessible by other replicas to relay DERP traffic. Required
Addresses for STUN servers to establish P2P connections. Set empty to disable P2P connections.
### --disable-owner-workspace-access
| | |
| ----------- | -------------------------------------------------- |
| Type | <code>bool</code> |
| Environment | <code>$CODER_DISABLE_OWNER_WORKSPACE_ACCESS</code> |
| YAML | <code>disableOwnerWorkspaceAccess</code> |
Remove the permission for the 'owner' role to have workspace execution on all workspaces. This prevents the 'owner' from ssh, apps, and terminal access based on the 'owner' role. They still have their user permissions to access their own workspaces.
### --disable-password-auth
| | |

View File

@ -58,7 +58,7 @@ func TestCheckACLPermissions(t *testing.T) {
params := map[string]codersdk.AuthorizationCheck{
updateSpecificTemplate: {
Object: codersdk.AuthorizationObject{
ResourceType: rbac.ResourceTemplate.Type,
ResourceType: codersdk.ResourceTemplate,
ResourceID: template.ID.String(),
},
Action: "write",

90
scripts/rbacgen/main.go Normal file
View File

@ -0,0 +1,90 @@
package main
import (
"bytes"
"context"
_ "embed"
"fmt"
"go/format"
"go/types"
"html/template"
"log"
"os"
"sort"
"golang.org/x/tools/go/packages"
)
//go:embed object.gotmpl
var objectGoTpl string
type TplState struct {
ResourceNames []string
}
// main will generate a file that lists all rbac objects.
// This is to provide an "AllResources" function that is always
// in sync.
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
path := "."
if len(os.Args) > 1 {
path = os.Args[1]
}
cfg := &packages.Config{
Mode: packages.NeedTypes | packages.NeedName | packages.NeedTypesInfo | packages.NeedDeps,
Tests: false,
Context: ctx,
}
pkgs, err := packages.Load(cfg, path)
if err != nil {
log.Fatalf("Failed to load package: %s", err.Error())
}
if len(pkgs) != 1 {
log.Fatalf("Expected 1 package, got %d", len(pkgs))
}
rbacPkg := pkgs[0]
if rbacPkg.Name != "rbac" {
log.Fatalf("Expected rbac package, got %q", rbacPkg.Name)
}
tpl, err := template.New("object.gotmpl").Parse(objectGoTpl)
if err != nil {
log.Fatalf("Failed to parse templates: %s", err.Error())
}
var out bytes.Buffer
err = tpl.Execute(&out, TplState{
ResourceNames: allResources(rbacPkg),
})
if err != nil {
log.Fatalf("Execute template: %s", err.Error())
}
formatted, err := format.Source(out.Bytes())
if err != nil {
log.Fatalf("Format template: %s", err.Error())
}
_, _ = fmt.Fprint(os.Stdout, string(formatted))
}
func allResources(pkg *packages.Package) []string {
var resources []string
names := pkg.Types.Scope().Names()
for _, name := range names {
obj, ok := pkg.Types.Scope().Lookup(name).(*types.Var)
if ok && obj.Type().String() == "github.com/coder/coder/coderd/rbac.Object" {
resources = append(resources, obj.Name())
}
}
sort.Strings(resources)
return resources
}

View File

@ -0,0 +1,12 @@
// Code generated by rbacgen/main.go. DO NOT EDIT.
package rbac
func AllResources() []Object {
return []Object{
{{- range .ResourceNames }}
{{ . }},
{{- end }}
}
}

View File

@ -117,7 +117,7 @@ export interface AuthorizationCheck {
// From codersdk/authorization.go
export interface AuthorizationObject {
readonly resource_type: string
readonly resource_type: RBACResource
readonly owner_id?: string
readonly organization_id?: string
readonly resource_id?: string
@ -379,6 +379,7 @@ export interface DeploymentValues {
readonly git_auth?: any
readonly config_ssh?: SSHConfig
readonly wgtunnel_host?: string
readonly disable_owner_workspace_exec?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.YAMLConfigPath")
readonly config?: string
readonly write_config?: boolean
@ -1421,6 +1422,55 @@ export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ["file"]
export type ProvisionerType = "echo" | "terraform"
export const ProvisionerTypes: ProvisionerType[] = ["echo", "terraform"]
// From codersdk/rbacresources.go
export type RBACResource =
| "api_key"
| "application_connect"
| "assign_org_role"
| "assign_role"
| "audit_log"
| "debug_info"
| "deployment_config"
| "deployment_stats"
| "file"
| "group"
| "license"
| "organization"
| "organization_member"
| "provisioner_daemon"
| "replicas"
| "system"
| "template"
| "user"
| "user_data"
| "workspace"
| "workspace_execution"
| "workspace_proxy"
export const RBACResources: RBACResource[] = [
"api_key",
"application_connect",
"assign_org_role",
"assign_role",
"audit_log",
"debug_info",
"deployment_config",
"deployment_stats",
"file",
"group",
"license",
"organization",
"organization_member",
"provisioner_daemon",
"replicas",
"system",
"template",
"user",
"user_data",
"workspace",
"workspace_execution",
"workspace_proxy",
]
// From codersdk/audit.go
export type ResourceType =
| "api_key"

View File

@ -21,6 +21,11 @@ export default {
usage: "something",
value: "1234",
},
{
name: "Disable Owner Workspace Execution",
usage: "something",
value: false,
},
{
name: "TLS Version",
usage: "something",
@ -52,6 +57,10 @@ NoTLS.args = {
name: "SSH Keygen Algorithm",
value: "1234",
} as DeploymentOption,
{
name: "Disable Owner Workspace Execution",
value: false,
} as DeploymentOption,
{
name: "Secure Auth Cookie",
value: "1234",

View File

@ -36,6 +36,7 @@ export const SecuritySettingsPageView = ({
options,
"SSH Keygen Algorithm",
"Secure Auth Cookie",
"Disable Owner Workspace Execution",
)}
/>
</div>

View File

@ -59,7 +59,7 @@ export const permissionsToCheck = {
},
[checks.viewDeploymentValues]: {
object: {
resource_type: "deployment_flags",
resource_type: "deployment_config",
},
action: "read",
},
@ -71,7 +71,7 @@ export const permissionsToCheck = {
},
[checks.viewUpdateCheck]: {
object: {
resource_type: "update_check",
resource_type: "deployment_config",
},
action: "read",
},