From 6e15df8ceb77992d6abe4033915e7dc215baea1b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 May 2024 16:56:11 -0500 Subject: [PATCH 01/24] wip --- coderd/rbac/object.go | 42 +++++------ coderd/rbac/object_gen.go | 101 ++++++++++++++++++-------- coderd/rbac/policy/policy.go | 131 ++++++++++++++++++++++++++++++++++ scripts/rbacgen/main.go | 102 ++++++++++++++++++-------- scripts/rbacgen/object.gotmpl | 22 ++++-- 5 files changed, 314 insertions(+), 84 deletions(-) diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index bac8b90fe90c4..9ba98a2194d53 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -18,17 +18,17 @@ type Objecter interface { var ( // ResourceWildcard represents all resource types // Try to avoid using this where possible. - ResourceWildcard = Object{ - Type: WildcardSymbol, - } + //ResourceWildcard = Object{ + // Type: WildcardSymbol, + //} // ResourceWorkspace CRUD. Org + User owner // create/delete = make or delete workspaces // read = access workspace // update = edit workspace variables - ResourceWorkspace = Object{ - Type: "workspace", - } + //ResourceWorkspace = Object{ + // Type: "workspace", + //} // ResourceWorkspaceBuild refers to permissions necessary to // insert a workspace build job. @@ -49,9 +49,9 @@ var ( // create/delete = make or delete proxies // read = read proxy urls // update = edit workspace proxy fields - ResourceWorkspaceProxy = Object{ - Type: "workspace_proxy", - } + //ResourceWorkspaceProxy = Object{ + // Type: "workspace_proxy", + //} // ResourceWorkspaceExecution CRUD. Org + User owner // create = workspace remote execution @@ -73,9 +73,9 @@ var ( // ResourceAuditLog // read = access audit log - ResourceAuditLog = Object{ - Type: "audit_log", - } + //ResourceAuditLog = Object{ + // Type: "audit_log", + //} // ResourceTemplate CRUD. Org owner only. // create/delete = Make or delete a new template @@ -170,22 +170,22 @@ var ( // create/delete = add or remove license from site. // read = view license claims // update = not applicable; licenses are immutable - ResourceLicense = Object{ - Type: "license", - } + //ResourceLicense = Object{ + // Type: "license", + //} // ResourceDeploymentValues ResourceDeploymentValues = Object{ Type: "deployment_config", } - ResourceDeploymentStats = Object{ - Type: "deployment_stats", - } + //ResourceDeploymentStats = Object{ + // Type: "deployment_stats", + //} - ResourceReplicas = Object{ - Type: "replicas", - } + //ResourceReplicas = Object{ + // Type: "replicas", + //} // ResourceDebugInfo controls access to the debug routes `/api/v2/debug/*`. ResourceDebugInfo = Object{ diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index b1cac5704e049..f3aa6df57588d 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -1,38 +1,83 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac +var ( + // ResourceWildcard + // Valid Actions + // - "*" needs [] :: Wildcard gives admin level access to all resources and all actions. + ResourceWildcard = Object{ + Type: "*", + } + + // ResourceWorkspace + // Valid Actions + // - "application_connect" needs [owner,org,acl] :: connect to workspace apps via browser + // - "create" needs [owner,org] :: create a workspace + // - "delete" needs [owner,org,acl] :: delete a workspace + // - "read" needs [owner,org,acl] :: read workspace data + // - "ssh" needs [owner,org,acl] :: ssh into a given workspace + // - "update" needs [owner,org,acl] :: update a workspace + ResourceWorkspace = Object{ + Type: "workspace", + } + + // ResourceWorkspaceProxy + // Valid Actions + // - "create" needs [] :: create a workspace proxy + // - "delete" needs [] :: delete a workspace proxy + // - "read" needs [] :: read and use a workspace proxy + // - "update" needs [] :: update a workspace proxy + ResourceWorkspaceProxy = Object{ + Type: "workspace_proxy", + } + + // ResourceLicense + // Valid Actions + // - "create" needs [] :: create a license + // - "delete" needs [] :: delete license + // - "read" needs [] :: read licenses + ResourceLicense = Object{ + Type: "license", + } + + // ResourceAuditLog + // Valid Actions + // - "read" needs [] :: read audit logs + ResourceAuditLog = Object{ + Type: "audit_log", + } + + // ResourceDeploymentConfig + // Valid Actions + // - "read" needs [] :: read deployment config + ResourceDeploymentConfig = Object{ + Type: "deployment_config", + } + + // ResourceDeploymentStats + // Valid Actions + // - "read" needs [] :: read deployment stats + ResourceDeploymentStats = Object{ + Type: "deployment_stats", + } + + // ResourceReplicas + // Valid Actions + // - "read" needs [] :: read replicas + ResourceReplicas = Object{ + Type: "replicas", + } +) + func AllResources() []Object { return []Object{ - ResourceAPIKey, - ResourceAuditLog, - ResourceDebugInfo, - ResourceDeploymentStats, - ResourceDeploymentValues, - ResourceFile, - ResourceGroup, - ResourceLicense, - ResourceOAuth2ProviderApp, - ResourceOAuth2ProviderAppCodeToken, - ResourceOAuth2ProviderAppSecret, - ResourceOrgRoleAssignment, - ResourceOrganization, - ResourceOrganizationMember, - ResourceProvisionerDaemon, - ResourceReplicas, - ResourceRoleAssignment, - ResourceSystem, - ResourceTailnetCoordinator, - ResourceTemplate, - ResourceTemplateInsights, - ResourceUser, - ResourceUserData, - ResourceUserWorkspaceBuildParameters, ResourceWildcard, ResourceWorkspace, - ResourceWorkspaceApplicationConnect, - ResourceWorkspaceBuild, - ResourceWorkspaceDormant, - ResourceWorkspaceExecution, ResourceWorkspaceProxy, + ResourceLicense, + ResourceAuditLog, + ResourceDeploymentConfig, + ResourceDeploymentStats, + ResourceReplicas, } } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index a3c0dc9f3436b..a893b2b755925 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -1,5 +1,11 @@ package policy +import "strings" + +const WildcardSymbol = "*" + +type actionFields uint32 + // Action represents the allowed actions to be done on an object. type Action string @@ -8,4 +14,129 @@ const ( ActionRead Action = "read" ActionUpdate Action = "update" ActionDelete Action = "delete" + + ActionUse Action = "use" + ActionSSH Action = "ssh" + ActionApplicationConnect = "application_connect" ) + +const ( + fieldOwner actionFields = 1 << iota + fieldOrg + fieldACL +) + +type PermissionDefinition struct { + // name is optional. Used to override "Type" for function naming. + name string + // Type should be a unique string to identify the + Type string + // Actions are a map of actions to some description of what the action + // should represent. The key in the actions map is the verb to use + // in the rbac policy. + Actions map[Action]ActionDefinition +} + +func (p PermissionDefinition) Name() string { + if p.name != "" { + return p.name + } + return p.Type +} + +type ActionDefinition struct { + // Human friendly description to explain the action. + Description string + + // These booleans enforce these fields are p + Fields actionFields +} + +func actDef(fields actionFields, description string) ActionDefinition { + return ActionDefinition{ + Description: description, + Fields: fields, + } +} + +func (a ActionDefinition) Requires() string { + fields := make([]string, 0) + if a.Fields&fieldOwner != 0 { + fields = append(fields, "owner") + } + if a.Fields&fieldOrg != 0 { + fields = append(fields, "org") + } + if a.Fields&fieldACL != 0 { + fields = append(fields, "acl") + } + + return strings.Join(fields, ",") +} + +var RBACPermissions = []PermissionDefinition{ + { + name: "Wildcard", + Type: WildcardSymbol, + Actions: map[Action]ActionDefinition{ + WildcardSymbol: { + Description: "Wildcard gives admin level access to all resources and all actions.", + Fields: 0, + }, + }, + }, + { + Type: "workspace", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(fieldOwner|fieldOrg, "create a workspace"), + ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data"), + // TODO: Make updates more granular + ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "update a workspace"), + ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete a workspace"), + ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"), + ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"), + }, + }, + { + Type: "workspace_proxy", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, "create a workspace proxy"), + ActionDelete: actDef(0, "delete a workspace proxy"), + ActionUpdate: actDef(0, "update a workspace proxy"), + ActionRead: actDef(0, "read and use a workspace proxy"), + }, + }, + { + Type: "license", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, "create a license"), + ActionRead: actDef(0, "read licenses"), + ActionDelete: actDef(0, "delete license"), + // Licenses are immutable, so update makes no sense + }, + }, + { + Type: "audit_log", + Actions: map[Action]ActionDefinition{ + ActionRead: actDef(0, "read audit logs"), + }, + }, + { + Type: "deployment_config", + Actions: map[Action]ActionDefinition{ + ActionRead: actDef(0, "read deployment config"), + }, + }, + { + Type: "deployment_stats", + Actions: map[Action]ActionDefinition{ + ActionRead: actDef(0, "read deployment stats"), + }, + }, + { + Type: "replicas", + Actions: map[Action]ActionDefinition{ + ActionRead: actDef(0, "read replicas"), + }, + }, +} diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index d237227f693dc..12b90782a5e23 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -11,8 +11,11 @@ import ( "log" "os" "sort" + "strings" "golang.org/x/tools/go/packages" + + "github.com/coder/coder/v2/coderd/rbac/policy" ) //go:embed object.gotmpl @@ -29,51 +32,90 @@ 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) + out := gen2(ctx) + formatted, err := format.Source(out) if err != nil { - log.Fatalf("Failed to load package: %s", err.Error()) + log.Fatalf("Format template: %s", err.Error()) } - if len(pkgs) != 1 { - log.Fatalf("Expected 1 package, got %d", len(pkgs)) - } + _, _ = fmt.Fprint(os.Stdout, string(formatted)) + return + + //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)) +} - rbacPkg := pkgs[0] - if rbacPkg.Name != "rbac" { - log.Fatalf("Expected rbac package, got %q", rbacPkg.Name) +func pascalCaseName(name string) string { + names := strings.Split(name, "_") + for i := range names { + names[i] = capitalize(names[i]) } + return strings.Join(names, "") +} + +func capitalize(name string) string { + return strings.ToUpper(string(name[0])) + name[1:] +} - tpl, err := template.New("object.gotmpl").Parse(objectGoTpl) +func gen2(ctx context.Context) []byte { + tpl, err := template.New("object.gotmpl").Funcs(template.FuncMap{ + "capitalize": capitalize, + "pascalCaseName": pascalCaseName, + }).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), - }) - + err = tpl.Execute(&out, policy.RBACPermissions) 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)) + return out.Bytes() } func allResources(pkg *packages.Package) []string { diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/object.gotmpl index 281acbc581925..7e99392bc8b91 100644 --- a/scripts/rbacgen/object.gotmpl +++ b/scripts/rbacgen/object.gotmpl @@ -1,12 +1,24 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac + +var ( + {{- range $element := . }} + // Resource{{ pascalCaseName $element.Name }} + // Valid Actions + {{- range $action, $value := .Actions }} + // - "{{ $action }}" needs [{{ $value.Requires }}] :: {{ $value.Description }} + {{- end }} + Resource{{ pascalCaseName $element.Name }} = Object { + Type: "{{ $element.Type }}", + } + {{ end -}} +) + func AllResources() []Object { return []Object{ - {{- range .ResourceNames }} - {{ . }}, - {{- end }} + {{- range $element := . }} + Resource{{ pascalCaseName $element.Name }}, + {{- end }} } } - - From ca7e8a5917ffc863fab49121f7d9681329c922cd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 May 2024 17:34:29 -0500 Subject: [PATCH 02/24] Fix readme --- coderd/rbac/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/rbac/README.md b/coderd/rbac/README.md index 2a73a59d7febc..e867fa9cce50a 100644 --- a/coderd/rbac/README.md +++ b/coderd/rbac/README.md @@ -106,7 +106,7 @@ You can test outside of golang by using the `opa` cli. **Evaluation** -opa eval --format=pretty 'false' -d policy.rego -i input.json +opa eval --format=pretty "data.authz.allow" -d policy.rego -i input.json **Partial Evaluation** From 4b69926c6dc5e5f3cace7a5f36d24177f3b89fd0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 May 2024 18:04:59 -0500 Subject: [PATCH 03/24] work on policy --- coderd/rbac/object.go | 74 +++++++++++++++++----------------- coderd/rbac/object_gen.go | 77 ++++++++++++++++++++++++++++++++++++ coderd/rbac/policy/policy.go | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 37 deletions(-) diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 9ba98a2194d53..acaa6b1c24757 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -58,18 +58,18 @@ var ( // read = ? // update = ? // delete = ? - ResourceWorkspaceExecution = Object{ - Type: "workspace_execution", - } + //ResourceWorkspaceExecution = Object{ + // Type: "workspace_execution", + //} // ResourceWorkspaceApplicationConnect CRUD. Org + User owner // create = connect to an application // read = ? // update = ? // delete = ? - ResourceWorkspaceApplicationConnect = Object{ - Type: "application_connect", - } + //ResourceWorkspaceApplicationConnect = Object{ + // Type: "application_connect", + //} // ResourceAuditLog // read = access audit log @@ -81,33 +81,33 @@ var ( // create/delete = Make or delete a new template // update = Update the template, make new template versions // read = read the template and all versions associated - ResourceTemplate = Object{ - Type: "template", - } + //ResourceTemplate = Object{ + // Type: "template", + //} // ResourceGroup CRUD. Org admins only. // create/delete = Make or delete a new group. // update = Update the name or members of a group. // read = Read groups and their members. - ResourceGroup = Object{ - Type: "group", - } + //ResourceGroup = Object{ + // Type: "group", + //} - ResourceFile = Object{ - Type: "file", - } + //ResourceFile = Object{ + // Type: "file", + //} - ResourceProvisionerDaemon = Object{ - Type: "provisioner_daemon", - } + //ResourceProvisionerDaemon = Object{ + // Type: "provisioner_daemon", + //} // ResourceOrganization CRUD. Has an org owner on all but 'create'. // create/delete = make or delete organizations // read = view org information (Can add user owner for read) // update = ?? - ResourceOrganization = Object{ - Type: "organization", - } + //ResourceOrganization = Object{ + // Type: "organization", + //} // ResourceRoleAssignment might be expanded later to allow more granular permissions // to modifying roles. For now, this covers all possible roles, so having this permission @@ -140,15 +140,15 @@ var ( // create/delete = make or delete a new user. // read = view all 'user' table data // update = update all 'user' table data - ResourceUser = Object{ - Type: "user", - } + //ResourceUser = Object{ + // Type: "user", + //} // ResourceUserData is any data associated with a user. A user has control // over their data (profile, password, etc). So this resource has an owner. - ResourceUserData = Object{ - Type: "user_data", - } + //ResourceUserData = Object{ + // Type: "user_data", + //} // ResourceUserWorkspaceBuildParameters is the user's workspace build // parameter history. @@ -161,9 +161,9 @@ var ( // create/delete = Create/delete member from org. // update = Update organization member // read = View member - ResourceOrganizationMember = Object{ - Type: "organization_member", - } + //ResourceOrganizationMember = Object{ + // Type: "organization_member", + //} // ResourceLicense is the license in the 'licenses' table. // ResourceLicense is site wide. @@ -175,9 +175,9 @@ var ( //} // ResourceDeploymentValues - ResourceDeploymentValues = Object{ - Type: "deployment_config", - } + //ResourceDeploymentValues = Object{ + // Type: "deployment_config", + //} //ResourceDeploymentStats = Object{ // Type: "deployment_stats", @@ -202,10 +202,10 @@ var ( Type: "tailnet_coordinator", } - // ResourceTemplateInsights is a pseudo-resource for reading template insights data. - ResourceTemplateInsights = Object{ - Type: "template_insights", - } + //// ResourceTemplateInsights is a pseudo-resource for reading template insights data. + //ResourceTemplateInsights = Object{ + // Type: "template_insights", + //} // ResourceOAuth2ProviderApp CRUD. // create/delete = Make or delete an OAuth2 app. diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index f3aa6df57588d..088abd003aa72 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -9,6 +9,18 @@ var ( Type: "*", } + // ResourceUser + // Valid Actions + // - "create" needs [] :: create a new user + // - "delete" needs [] :: delete an existing user + // - "read" needs [] :: read user data + // - "read_personal" needs [owner] :: read personal user data like password + // - "update" needs [] :: update an existing user + // - "update_personal" needs [owner] :: update personal data + ResourceUser = Object{ + Type: "user", + } + // ResourceWorkspace // Valid Actions // - "application_connect" needs [owner,org,acl] :: connect to workspace apps via browser @@ -67,11 +79,70 @@ var ( ResourceReplicas = Object{ Type: "replicas", } + + // ResourceTemplate + // Valid Actions + // - "create" needs [org] :: create a template + // - "delete" needs [org,acl] :: delete a template + // - "read" needs [org,acl] :: read template + // - "update" needs [org,acl] :: update a template + // - "view_insights" needs [org,acl] :: view insights + ResourceTemplate = Object{ + Type: "template", + } + + // ResourceGroup + // Valid Actions + // - "create" needs [org] :: create a group + // - "delete" needs [org] :: delete a group + // - "read" needs [org] :: read groups + // - "update" needs [org] :: update a group + ResourceGroup = Object{ + Type: "group", + } + + // ResourceFile + // Valid Actions + // - "create" needs [] :: create a file + // - "read" needs [] :: read files + ResourceFile = Object{ + Type: "file", + } + + // ResourceProvisionerDaemon + // Valid Actions + // - "create" needs [org] :: create a provisioner daemon + // - "delete" needs [org] :: delete a provisioner daemon + // - "read" needs [org] :: read provisioner daemon + // - "update" needs [org] :: update a provisioner daemon + ResourceProvisionerDaemon = Object{ + Type: "provisioner_daemon", + } + + // ResourceOrganization + // Valid Actions + // - "create" needs [] :: create an organization + // - "delete" needs [] :: delete a organization + // - "read" needs [] :: read organizations + ResourceOrganization = Object{ + Type: "organization", + } + + // ResourceOrganizationMember + // Valid Actions + // - "create" needs [org] :: create an organization member + // - "delete" needs [org] :: delete member + // - "read" needs [org] :: read member + // - "update" needs [org] :: update a organization member + ResourceOrganizationMember = Object{ + Type: "organization_member", + } ) func AllResources() []Object { return []Object{ ResourceWildcard, + ResourceUser, ResourceWorkspace, ResourceWorkspaceProxy, ResourceLicense, @@ -79,5 +150,11 @@ func AllResources() []Object { ResourceDeploymentConfig, ResourceDeploymentStats, ResourceReplicas, + ResourceTemplate, + ResourceGroup, + ResourceFile, + ResourceProvisionerDaemon, + ResourceOrganization, + ResourceOrganizationMember, } } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index a893b2b755925..6a0f9b1fe795a 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -18,6 +18,7 @@ const ( ActionUse Action = "use" ActionSSH Action = "ssh" ActionApplicationConnect = "application_connect" + ActionViewInsights = "view_insights" ) const ( @@ -85,6 +86,20 @@ var RBACPermissions = []PermissionDefinition{ }, }, }, + { + Type: "user", + Actions: map[Action]ActionDefinition{ + // Actions deal with site wide user objects. + ActionRead: actDef(0, "read user data"), + ActionCreate: actDef(0, "create a new user"), + ActionUpdate: actDef(0, "update an existing user"), + ActionDelete: actDef(0, "delete an existing user"), + + "read_personal": actDef(fieldOwner, "read personal user data like password"), + "update_personal": actDef(fieldOwner, "update personal data"), + //ActionReadPublic: actDef(fieldOwner, "read public user data"), + }, + }, { Type: "workspace", Actions: map[Action]ActionDefinition{ @@ -139,4 +154,58 @@ var RBACPermissions = []PermissionDefinition{ ActionRead: actDef(0, "read replicas"), }, }, + { + Type: "template", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(fieldOrg, "create a template"), + // TODO: Create a use permission maybe? + ActionRead: actDef(fieldOrg|fieldACL, "read template"), + ActionUpdate: actDef(fieldOrg|fieldACL, "update a template"), + ActionDelete: actDef(fieldOrg|fieldACL, "delete a template"), + ActionViewInsights: actDef(fieldOrg|fieldACL, "view insights"), + }, + }, + { + Type: "group", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(fieldOrg, "create a group"), + ActionRead: actDef(fieldOrg, "read groups"), + ActionDelete: actDef(fieldOrg, "delete a group"), + ActionUpdate: actDef(fieldOrg, "update a group"), + }, + }, + { + Type: "file", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, "create a file"), + ActionRead: actDef(0, "read files"), + }, + }, + { + Type: "provisioner_daemon", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(fieldOrg, "create a provisioner daemon"), + // TODO: Move to use? + ActionRead: actDef(fieldOrg, "read provisioner daemon"), + ActionUpdate: actDef(fieldOrg, "update a provisioner daemon"), + ActionDelete: actDef(fieldOrg, "delete a provisioner daemon"), + }, + }, + { + Type: "organization", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, "create an organization"), + ActionRead: actDef(0, "read organizations"), + ActionDelete: actDef(0, "delete a organization"), + }, + }, + { + Type: "organization_member", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(fieldOrg, "create an organization member"), + ActionRead: actDef(fieldOrg, "read member"), + ActionUpdate: actDef(fieldOrg, "update a organization member"), + ActionDelete: actDef(fieldOrg, "delete member"), + }, + }, } From a9b89849c6c1d8dc7f02569aa303509eba95f5b4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 13 May 2024 15:05:24 -0500 Subject: [PATCH 04/24] WIP: delete all old resources, generating them from a policy --- coderd/coderdtest/authorize.go | 7 -- coderd/rbac/object.go | 219 --------------------------------- coderd/rbac/object_gen.go | 108 +++++++++++++++- coderd/rbac/policy/policy.go | 110 +++++++++++++++-- 4 files changed, 207 insertions(+), 237 deletions(-) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index 6c38063a0dbbe..e753e66f2d2f6 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -416,23 +416,16 @@ func RandomRBACObject() rbac.Object { func randomRBACType() string { all := []string{ rbac.ResourceWorkspace.Type, - rbac.ResourceWorkspaceExecution.Type, - rbac.ResourceWorkspaceApplicationConnect.Type, rbac.ResourceAuditLog.Type, rbac.ResourceTemplate.Type, rbac.ResourceGroup.Type, rbac.ResourceFile.Type, rbac.ResourceProvisionerDaemon.Type, rbac.ResourceOrganization.Type, - rbac.ResourceRoleAssignment.Type, - rbac.ResourceOrgRoleAssignment.Type, - rbac.ResourceAPIKey.Type, rbac.ResourceUser.Type, - rbac.ResourceUserData.Type, rbac.ResourceOrganizationMember.Type, rbac.ResourceWildcard.Type, rbac.ResourceLicense.Type, - rbac.ResourceDeploymentValues.Type, rbac.ResourceReplicas.Type, rbac.ResourceDebugInfo.Type, } diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index acaa6b1c24757..a0eb962cab4f4 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -13,225 +13,6 @@ type Objecter interface { RBACObject() Object } -// 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 - // update = edit workspace variables - //ResourceWorkspace = Object{ - // Type: "workspace", - //} - - // ResourceWorkspaceBuild refers to permissions necessary to - // insert a workspace build job. - // create/delete = ? - // read = read workspace builds - // update = insert/update workspace builds. - ResourceWorkspaceBuild = Object{ - Type: "workspace_build", - } - - // ResourceWorkspaceDormant is returned if a workspace is dormant. - // It grants restricted permissions on workspace builds. - ResourceWorkspaceDormant = Object{ - Type: "workspace_dormant", - } - - // ResourceWorkspaceProxy CRUD. Org - // create/delete = make or delete proxies - // read = read proxy urls - // update = edit workspace proxy fields - //ResourceWorkspaceProxy = Object{ - // Type: "workspace_proxy", - //} - - // ResourceWorkspaceExecution CRUD. Org + User owner - // create = workspace remote execution - // read = ? - // update = ? - // delete = ? - //ResourceWorkspaceExecution = Object{ - // Type: "workspace_execution", - //} - - // ResourceWorkspaceApplicationConnect CRUD. Org + User owner - // create = connect to an application - // read = ? - // update = ? - // delete = ? - //ResourceWorkspaceApplicationConnect = Object{ - // Type: "application_connect", - //} - - // ResourceAuditLog - // read = access audit log - //ResourceAuditLog = Object{ - // Type: "audit_log", - //} - - // ResourceTemplate CRUD. Org owner only. - // create/delete = Make or delete a new template - // update = Update the template, make new template versions - // read = read the template and all versions associated - //ResourceTemplate = Object{ - // Type: "template", - //} - - // ResourceGroup CRUD. Org admins only. - // create/delete = Make or delete a new group. - // update = Update the name or members of a group. - // read = Read groups and their members. - //ResourceGroup = Object{ - // Type: "group", - //} - - //ResourceFile = Object{ - // Type: "file", - //} - - //ResourceProvisionerDaemon = Object{ - // Type: "provisioner_daemon", - //} - - // ResourceOrganization CRUD. Has an org owner on all but 'create'. - // create/delete = make or delete organizations - // read = view org information (Can add user owner for read) - // update = ?? - //ResourceOrganization = Object{ - // Type: "organization", - //} - - // ResourceRoleAssignment might be expanded later to allow more granular permissions - // to modifying roles. For now, this covers all possible roles, so having this permission - // allows granting/deleting **ALL** roles. - // Never has an owner or org. - // create = Assign roles - // update = ?? - // read = View available roles to assign - // delete = Remove role - ResourceRoleAssignment = Object{ - Type: "assign_role", - } - - // ResourceOrgRoleAssignment is just like ResourceRoleAssignment but for organization roles. - ResourceOrgRoleAssignment = Object{ - Type: "assign_org_role", - } - - // ResourceAPIKey is owned by a user. - // create = Create a new api key for user - // update = ?? - // read = View api key - // delete = Delete api key - ResourceAPIKey = Object{ - Type: "api_key", - } - - // ResourceUser is the user in the 'users' table. - // ResourceUser never has any owners or in an org, as it's site wide. - // create/delete = make or delete a new user. - // read = view all 'user' table data - // update = update all 'user' table data - //ResourceUser = Object{ - // Type: "user", - //} - - // ResourceUserData is any data associated with a user. A user has control - // over their data (profile, password, etc). So this resource has an owner. - //ResourceUserData = Object{ - // Type: "user_data", - //} - - // ResourceUserWorkspaceBuildParameters is the user's workspace build - // parameter history. - ResourceUserWorkspaceBuildParameters = Object{ - Type: "user_workspace_build_parameters", - } - - // ResourceOrganizationMember is a user's membership in an organization. - // Has ONLY an organization owner. - // create/delete = Create/delete member from org. - // update = Update organization member - // read = View member - //ResourceOrganizationMember = Object{ - // Type: "organization_member", - //} - - // ResourceLicense is the license in the 'licenses' table. - // ResourceLicense is site wide. - // create/delete = add or remove license from site. - // read = view license claims - // update = not applicable; licenses are immutable - //ResourceLicense = Object{ - // Type: "license", - //} - - // ResourceDeploymentValues - //ResourceDeploymentValues = Object{ - // Type: "deployment_config", - //} - - //ResourceDeploymentStats = Object{ - // Type: "deployment_stats", - //} - - //ResourceReplicas = Object{ - // Type: "replicas", - //} - - // ResourceDebugInfo controls access to the debug routes `/api/v2/debug/*`. - ResourceDebugInfo = Object{ - Type: "debug_info", - } - - // ResourceSystem is a pseudo-resource only used for system-level actions. - ResourceSystem = Object{ - Type: "system", - } - - // ResourceTailnetCoordinator is a pseudo-resource for use by the tailnet coordinator - ResourceTailnetCoordinator = Object{ - Type: "tailnet_coordinator", - } - - //// ResourceTemplateInsights is a pseudo-resource for reading template insights data. - //ResourceTemplateInsights = Object{ - // Type: "template_insights", - //} - - // ResourceOAuth2ProviderApp CRUD. - // create/delete = Make or delete an OAuth2 app. - // update = Update the properties of the OAuth2 app. - // read = Read OAuth2 apps. - ResourceOAuth2ProviderApp = Object{ - Type: "oauth2_app", - } - - // ResourceOAuth2ProviderAppSecret CRUD. - // create/delete = Make or delete an OAuth2 app secret. - // update = Update last used date. - // read = Read OAuth2 app hashed or truncated secret. - ResourceOAuth2ProviderAppSecret = Object{ - Type: "oauth2_app_secret", - } - - // ResourceOAuth2ProviderAppCodeToken CRUD. - // create/delete = Make or delete an OAuth2 app code or token. - // update = None - // read = Check if OAuth2 app code or token exists. - ResourceOAuth2ProviderAppCodeToken = Object{ - Type: "oauth2_app_code_token", - } -) - // ResourceUserObject is a helper function to create a user object for authz checks. func ResourceUserObject(userID uuid.UUID) Object { return ResourceUser.WithID(userID).WithOwner(userID.String()) diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 088abd003aa72..343d5efb7b433 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -24,15 +24,23 @@ var ( // ResourceWorkspace // Valid Actions // - "application_connect" needs [owner,org,acl] :: connect to workspace apps via browser - // - "create" needs [owner,org] :: create a workspace - // - "delete" needs [owner,org,acl] :: delete a workspace - // - "read" needs [owner,org,acl] :: read workspace data + // - "build" needs [owner,org,acl] :: allows starting, stopping, and updating a workspace + // - "build_parameters" needs [owner,org,acl] :: view workspace build parameters + // - "create" needs [owner,org] :: create a new workspace + // - "delete" needs [owner,org,acl] :: delete workspace + // - "read" needs [owner,org,acl] :: read workspace data to view on the UI // - "ssh" needs [owner,org,acl] :: ssh into a given workspace - // - "update" needs [owner,org,acl] :: update a workspace + // - "update" needs [owner,org,acl] :: edit workspace settings (scheduling, permissions, parameters) ResourceWorkspace = Object{ Type: "workspace", } + // ResourceWorkspaceDormant + // Valid Actions + ResourceWorkspaceDormant = Object{ + Type: "workspace_dormant", + } + // ResourceWorkspaceProxy // Valid Actions // - "create" needs [] :: create a workspace proxy @@ -137,6 +145,88 @@ var ( ResourceOrganizationMember = Object{ Type: "organization_member", } + + // ResourceDebugInfo + // Valid Actions + // - "use" needs [] :: access to debug routes + ResourceDebugInfo = Object{ + Type: "debug_info", + } + + // ResourceSystem + // Valid Actions + // - "create" needs [] :: create system resources + // - "delete" needs [] :: delete system resources + // - "read" needs [] :: view system resources + // - "update" needs [] :: update system resources + ResourceSystem = Object{ + Type: "system", + } + + // ResourceApiKey + // Valid Actions + // - "create" needs [owner] :: create an api key + // - "delete" needs [owner] :: delete an api key + // - "read" needs [owner] :: read api key details (secrets are not stored) + ResourceApiKey = Object{ + Type: "api_key", + } + + // ResourceTailnetCoordinator + // Valid Actions + // - "create" needs [] :: + // - "delete" needs [] :: + // - "read" needs [] :: + // - "update" needs [] :: + ResourceTailnetCoordinator = Object{ + Type: "tailnet_coordinator", + } + + // ResourceAssignRole + // Valid Actions + // - "assign" needs [] :: ability to assign roles + // - "delete" needs [] :: ability to delete roles + // - "read" needs [] :: view what roles are assignable + ResourceAssignRole = Object{ + Type: "assign_role", + } + + // ResourceAssignOrgRole + // Valid Actions + // - "assign" needs [] :: ability to assign org scoped roles + // - "delete" needs [] :: ability to delete org scoped roles + ResourceAssignOrgRole = Object{ + Type: "assign_org_role", + } + + // ResourceOauth2App + // Valid Actions + // - "create" needs [] :: make an OAuth2 app. + // - "delete" needs [] :: delete an OAuth2 app + // - "read" needs [] :: read OAuth2 apps + // - "update" needs [] :: update the properties of the OAuth2 app. + ResourceOauth2App = Object{ + Type: "oauth2_app", + } + + // ResourceOauth2AppSecret + // Valid Actions + // - "create" needs [] :: + // - "delete" needs [] :: + // - "read" needs [] :: + // - "update" needs [] :: + ResourceOauth2AppSecret = Object{ + Type: "oauth2_app_secret", + } + + // ResourceOauth2AppCodeToken + // Valid Actions + // - "create" needs [] :: + // - "delete" needs [] :: + // - "read" needs [] :: + ResourceOauth2AppCodeToken = Object{ + Type: "oauth2_app_code_token", + } ) func AllResources() []Object { @@ -144,6 +234,7 @@ func AllResources() []Object { ResourceWildcard, ResourceUser, ResourceWorkspace, + ResourceWorkspaceDormant, ResourceWorkspaceProxy, ResourceLicense, ResourceAuditLog, @@ -156,5 +247,14 @@ func AllResources() []Object { ResourceProvisionerDaemon, ResourceOrganization, ResourceOrganizationMember, + ResourceDebugInfo, + ResourceSystem, + ResourceApiKey, + ResourceTailnetCoordinator, + ResourceAssignRole, + ResourceAssignOrgRole, + ResourceOauth2App, + ResourceOauth2AppSecret, + ResourceOauth2AppCodeToken, } } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 6a0f9b1fe795a..667150b19d713 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -17,13 +17,24 @@ const ( ActionUse Action = "use" ActionSSH Action = "ssh" - ActionApplicationConnect = "application_connect" - ActionViewInsights = "view_insights" + ActionApplicationConnect Action = "application_connect" + ActionViewInsights Action = "view_insights" + + ActionWorkspaceBuild Action = "build" + ActionViewWorkspaceBuildParams Action = "build_parameters" + + ActionAssign Action = "assign" ) const ( - fieldOwner actionFields = 1 << iota + // What fields are expected for a given action. + // fieldID: uuid for the resource + fieldID actionFields = 1 << iota + // fieldOwner: expects an 'Owner' value + fieldOwner + // fieldOrg: expects the resource to be owned by an org fieldOrg + // fieldACL: expects an ACL list to accompany the object fieldACL ) @@ -103,15 +114,27 @@ var RBACPermissions = []PermissionDefinition{ { Type: "workspace", Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOwner|fieldOrg, "create a workspace"), - ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data"), + ActionCreate: actDef(fieldOwner|fieldOrg, "create a new workspace"), + ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data to view on the UI"), // TODO: Make updates more granular - ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "update a workspace"), - ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete a workspace"), + ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "edit workspace settings (scheduling, permissions, parameters)"), + ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete workspace"), + + // Workspace provisioning + ActionWorkspaceBuild: actDef(fieldOwner|fieldOrg|fieldACL, "allows starting, stopping, and updating a workspace"), + // TODO: ActionViewWorkspaceBuildParams is very werid. Seems to be used for autofilling the last params set. + // Admins want this so they can update a user's workspace with the old values?? + ActionViewWorkspaceBuildParams: actDef(fieldOwner|fieldOrg|fieldACL, "view workspace build parameters"), + + // Running a workspace ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"), ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"), }, }, + { + Type: "workspace_dormant", + Actions: map[Action]ActionDefinition{}, + }, { Type: "workspace_proxy", Actions: map[Action]ActionDefinition{ @@ -208,4 +231,77 @@ var RBACPermissions = []PermissionDefinition{ ActionDelete: actDef(fieldOrg, "delete member"), }, }, + { + Type: "debug_info", + Actions: map[Action]ActionDefinition{ + ActionUse: actDef(0, "access to debug routes"), + }, + }, + { + Type: "system", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, "create system resources"), + ActionRead: actDef(0, "view system resources"), + ActionUpdate: actDef(0, "update system resources"), + ActionDelete: actDef(0, "delete system resources"), + }, + }, + { + Type: "api_key", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(fieldOwner, "create an api key"), + ActionRead: actDef(fieldOwner, "read api key details (secrets are not stored)"), + ActionDelete: actDef(fieldOwner, "delete an api key"), + }, + }, + { + Type: "tailnet_coordinator", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, ""), + ActionRead: actDef(0, ""), + ActionUpdate: actDef(0, ""), + ActionDelete: actDef(0, ""), + }, + }, + { + Type: "assign_role", + Actions: map[Action]ActionDefinition{ + ActionAssign: actDef(0, "ability to assign roles"), + ActionRead: actDef(0, "view what roles are assignable"), + ActionDelete: actDef(0, "ability to delete roles"), + }, + }, + { + Type: "assign_org_role", + Actions: map[Action]ActionDefinition{ + ActionAssign: actDef(0, "ability to assign org scoped roles"), + ActionDelete: actDef(0, "ability to delete org scoped roles"), + }, + }, + { + Type: "oauth2_app", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, "make an OAuth2 app."), + ActionRead: actDef(0, "read OAuth2 apps"), + ActionUpdate: actDef(0, "update the properties of the OAuth2 app."), + ActionDelete: actDef(0, "delete an OAuth2 app"), + }, + }, + { + Type: "oauth2_app_secret", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, ""), + ActionRead: actDef(0, ""), + ActionUpdate: actDef(0, ""), + ActionDelete: actDef(0, ""), + }, + }, + { + Type: "oauth2_app_code_token", + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(0, ""), + ActionRead: actDef(0, ""), + ActionDelete: actDef(0, ""), + }, + }, } From 48f05930c248a9ea513b185e236587331510e1c3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 13 May 2024 15:21:22 -0500 Subject: [PATCH 05/24] Start the move to policy.Action --- coderd/rbac/authz.go | 5 ----- coderd/rbac/scopes.go | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index c647bb09f89a0..78341fa1d1ef5 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -26,11 +26,6 @@ import ( "github.com/coder/coder/v2/coderd/util/slice" ) -// AllActions is a helper function to return all the possible actions types. -func AllActions() []policy.Action { - return []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} -} - type AuthCall struct { Actor Subject Action policy.Action diff --git a/coderd/rbac/scopes.go b/coderd/rbac/scopes.go index 6353ca3c67919..a0b77756a36fe 100644 --- a/coderd/rbac/scopes.go +++ b/coderd/rbac/scopes.go @@ -74,7 +74,7 @@ var builtinScopes = map[ScopeName]Scope{ Name: fmt.Sprintf("Scope_%s", ScopeApplicationConnect), DisplayName: "Ability to connect to applications", Site: Permissions(map[string][]policy.Action{ - ResourceWorkspaceApplicationConnect.Type: {policy.ActionCreate}, + ResourceWorkspace.Type: {policy.ActionApplicationConnect}, }), Org: map[string][]Permission{}, User: []Permission{}, From 6964513e1163f59a497d854de469c8b9ce898247 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 13 May 2024 15:59:40 -0500 Subject: [PATCH 06/24] update gen to be sorted --- coderd/rbac/object.go | 4 + coderd/rbac/object_gen.go | 258 +++++++++++++++++----------------- coderd/rbac/policy/policy.go | 94 +++++-------- scripts/rbacgen/main.go | 98 ++++--------- scripts/rbacgen/object.gotmpl | 7 +- 5 files changed, 197 insertions(+), 264 deletions(-) diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index a0eb962cab4f4..dbd873917bc27 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -37,6 +37,10 @@ type Object struct { ACLGroupList map[string][]policy.Action ` json:"acl_group_list"` } +func (z Object) AvailableActions() []policy.Action { + policy.Action() +} + func (z Object) Equal(b Object) bool { if z.ID != b.ID { return false diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 343d5efb7b433..facb7472e58c3 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -9,55 +9,30 @@ var ( Type: "*", } - // ResourceUser - // Valid Actions - // - "create" needs [] :: create a new user - // - "delete" needs [] :: delete an existing user - // - "read" needs [] :: read user data - // - "read_personal" needs [owner] :: read personal user data like password - // - "update" needs [] :: update an existing user - // - "update_personal" needs [owner] :: update personal data - ResourceUser = Object{ - Type: "user", - } - - // ResourceWorkspace - // Valid Actions - // - "application_connect" needs [owner,org,acl] :: connect to workspace apps via browser - // - "build" needs [owner,org,acl] :: allows starting, stopping, and updating a workspace - // - "build_parameters" needs [owner,org,acl] :: view workspace build parameters - // - "create" needs [owner,org] :: create a new workspace - // - "delete" needs [owner,org,acl] :: delete workspace - // - "read" needs [owner,org,acl] :: read workspace data to view on the UI - // - "ssh" needs [owner,org,acl] :: ssh into a given workspace - // - "update" needs [owner,org,acl] :: edit workspace settings (scheduling, permissions, parameters) - ResourceWorkspace = Object{ - Type: "workspace", - } - - // ResourceWorkspaceDormant + // ResourceApiKey // Valid Actions - ResourceWorkspaceDormant = Object{ - Type: "workspace_dormant", + // - "create" needs [owner] :: create an api key + // - "delete" needs [owner] :: delete an api key + // - "read" needs [owner] :: read api key details (secrets are not stored) + ResourceApiKey = Object{ + Type: "api_key", } - // ResourceWorkspaceProxy + // ResourceAssignOrgRole // Valid Actions - // - "create" needs [] :: create a workspace proxy - // - "delete" needs [] :: delete a workspace proxy - // - "read" needs [] :: read and use a workspace proxy - // - "update" needs [] :: update a workspace proxy - ResourceWorkspaceProxy = Object{ - Type: "workspace_proxy", + // - "assign" needs [] :: ability to assign org scoped roles + // - "delete" needs [] :: ability to delete org scoped roles + ResourceAssignOrgRole = Object{ + Type: "assign_org_role", } - // ResourceLicense + // ResourceAssignRole // Valid Actions - // - "create" needs [] :: create a license - // - "delete" needs [] :: delete license - // - "read" needs [] :: read licenses - ResourceLicense = Object{ - Type: "license", + // - "assign" needs [] :: ability to assign roles + // - "delete" needs [] :: ability to delete roles + // - "read" needs [] :: view what roles are assignable + ResourceAssignRole = Object{ + Type: "assign_role", } // ResourceAuditLog @@ -67,6 +42,13 @@ var ( Type: "audit_log", } + // ResourceDebugInfo + // Valid Actions + // - "use" needs [] :: access to debug routes + ResourceDebugInfo = Object{ + Type: "debug_info", + } + // ResourceDeploymentConfig // Valid Actions // - "read" needs [] :: read deployment config @@ -81,22 +63,12 @@ var ( Type: "deployment_stats", } - // ResourceReplicas - // Valid Actions - // - "read" needs [] :: read replicas - ResourceReplicas = Object{ - Type: "replicas", - } - - // ResourceTemplate + // ResourceFile // Valid Actions - // - "create" needs [org] :: create a template - // - "delete" needs [org,acl] :: delete a template - // - "read" needs [org,acl] :: read template - // - "update" needs [org,acl] :: update a template - // - "view_insights" needs [org,acl] :: view insights - ResourceTemplate = Object{ - Type: "template", + // - "create" needs [] :: create a file + // - "read" needs [] :: read files + ResourceFile = Object{ + Type: "file", } // ResourceGroup @@ -109,22 +81,42 @@ var ( Type: "group", } - // ResourceFile + // ResourceLicense // Valid Actions - // - "create" needs [] :: create a file - // - "read" needs [] :: read files - ResourceFile = Object{ - Type: "file", + // - "create" needs [] :: create a license + // - "delete" needs [] :: delete license + // - "read" needs [] :: read licenses + ResourceLicense = Object{ + Type: "license", } - // ResourceProvisionerDaemon + // ResourceOauth2App // Valid Actions - // - "create" needs [org] :: create a provisioner daemon - // - "delete" needs [org] :: delete a provisioner daemon - // - "read" needs [org] :: read provisioner daemon - // - "update" needs [org] :: update a provisioner daemon - ResourceProvisionerDaemon = Object{ - Type: "provisioner_daemon", + // - "create" needs [] :: make an OAuth2 app. + // - "delete" needs [] :: delete an OAuth2 app + // - "read" needs [] :: read OAuth2 apps + // - "update" needs [] :: update the properties of the OAuth2 app. + ResourceOauth2App = Object{ + Type: "oauth2_app", + } + + // ResourceOauth2AppCodeToken + // Valid Actions + // - "create" needs [] :: + // - "delete" needs [] :: + // - "read" needs [] :: + ResourceOauth2AppCodeToken = Object{ + Type: "oauth2_app_code_token", + } + + // ResourceOauth2AppSecret + // Valid Actions + // - "create" needs [] :: + // - "delete" needs [] :: + // - "read" needs [] :: + // - "update" needs [] :: + ResourceOauth2AppSecret = Object{ + Type: "oauth2_app_secret", } // ResourceOrganization @@ -146,11 +138,21 @@ var ( Type: "organization_member", } - // ResourceDebugInfo + // ResourceProvisionerDaemon // Valid Actions - // - "use" needs [] :: access to debug routes - ResourceDebugInfo = Object{ - Type: "debug_info", + // - "create" needs [org] :: create a provisioner daemon + // - "delete" needs [org] :: delete a provisioner daemon + // - "read" needs [org] :: read provisioner daemon + // - "update" needs [org] :: update a provisioner daemon + ResourceProvisionerDaemon = Object{ + Type: "provisioner_daemon", + } + + // ResourceReplicas + // Valid Actions + // - "read" needs [] :: read replicas + ResourceReplicas = Object{ + Type: "replicas", } // ResourceSystem @@ -163,15 +165,6 @@ var ( Type: "system", } - // ResourceApiKey - // Valid Actions - // - "create" needs [owner] :: create an api key - // - "delete" needs [owner] :: delete an api key - // - "read" needs [owner] :: read api key details (secrets are not stored) - ResourceApiKey = Object{ - Type: "api_key", - } - // ResourceTailnetCoordinator // Valid Actions // - "create" needs [] :: @@ -182,79 +175,86 @@ var ( Type: "tailnet_coordinator", } - // ResourceAssignRole + // ResourceTemplate // Valid Actions - // - "assign" needs [] :: ability to assign roles - // - "delete" needs [] :: ability to delete roles - // - "read" needs [] :: view what roles are assignable - ResourceAssignRole = Object{ - Type: "assign_role", + // - "create" needs [org] :: create a template + // - "delete" needs [org,acl] :: delete a template + // - "read" needs [org,acl] :: read template + // - "update" needs [org,acl] :: update a template + // - "view_insights" needs [org,acl] :: view insights + ResourceTemplate = Object{ + Type: "template", } - // ResourceAssignOrgRole + // ResourceUser // Valid Actions - // - "assign" needs [] :: ability to assign org scoped roles - // - "delete" needs [] :: ability to delete org scoped roles - ResourceAssignOrgRole = Object{ - Type: "assign_org_role", + // - "create" needs [] :: create a new user + // - "delete" needs [] :: delete an existing user + // - "read" needs [] :: read user data + // - "read_personal" needs [owner] :: read personal user data like password + // - "update" needs [] :: update an existing user + // - "update_personal" needs [owner] :: update personal data + ResourceUser = Object{ + Type: "user", } - // ResourceOauth2App + // ResourceWorkspace // Valid Actions - // - "create" needs [] :: make an OAuth2 app. - // - "delete" needs [] :: delete an OAuth2 app - // - "read" needs [] :: read OAuth2 apps - // - "update" needs [] :: update the properties of the OAuth2 app. - ResourceOauth2App = Object{ - Type: "oauth2_app", + // - "application_connect" needs [owner,org,acl] :: connect to workspace apps via browser + // - "build" needs [owner,org,acl] :: allows starting, stopping, and updating a workspace + // - "build_parameters" needs [owner,org,acl] :: view workspace build parameters + // - "create" needs [owner,org] :: create a new workspace + // - "delete" needs [owner,org,acl] :: delete workspace + // - "read" needs [owner,org,acl] :: read workspace data to view on the UI + // - "ssh" needs [owner,org,acl] :: ssh into a given workspace + // - "update" needs [owner,org,acl] :: edit workspace settings (scheduling, permissions, parameters) + ResourceWorkspace = Object{ + Type: "workspace", } - // ResourceOauth2AppSecret + // ResourceWorkspaceDormant // Valid Actions - // - "create" needs [] :: - // - "delete" needs [] :: - // - "read" needs [] :: - // - "update" needs [] :: - ResourceOauth2AppSecret = Object{ - Type: "oauth2_app_secret", + ResourceWorkspaceDormant = Object{ + Type: "workspace_dormant", } - // ResourceOauth2AppCodeToken + // ResourceWorkspaceProxy // Valid Actions - // - "create" needs [] :: - // - "delete" needs [] :: - // - "read" needs [] :: - ResourceOauth2AppCodeToken = Object{ - Type: "oauth2_app_code_token", + // - "create" needs [] :: create a workspace proxy + // - "delete" needs [] :: delete a workspace proxy + // - "read" needs [] :: read and use a workspace proxy + // - "update" needs [] :: update a workspace proxy + ResourceWorkspaceProxy = Object{ + Type: "workspace_proxy", } ) func AllResources() []Object { return []Object{ ResourceWildcard, - ResourceUser, - ResourceWorkspace, - ResourceWorkspaceDormant, - ResourceWorkspaceProxy, - ResourceLicense, + ResourceApiKey, + ResourceAssignOrgRole, + ResourceAssignRole, ResourceAuditLog, + ResourceDebugInfo, ResourceDeploymentConfig, ResourceDeploymentStats, - ResourceReplicas, - ResourceTemplate, - ResourceGroup, ResourceFile, - ResourceProvisionerDaemon, + ResourceGroup, + ResourceLicense, + ResourceOauth2App, + ResourceOauth2AppCodeToken, + ResourceOauth2AppSecret, ResourceOrganization, ResourceOrganizationMember, - ResourceDebugInfo, + ResourceProvisionerDaemon, + ResourceReplicas, ResourceSystem, - ResourceApiKey, ResourceTailnetCoordinator, - ResourceAssignRole, - ResourceAssignOrgRole, - ResourceOauth2App, - ResourceOauth2AppSecret, - ResourceOauth2AppCodeToken, + ResourceTemplate, + ResourceUser, + ResourceWorkspace, + ResourceWorkspaceDormant, + ResourceWorkspaceProxy, } } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 667150b19d713..bee869afa7daa 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -40,22 +40,13 @@ const ( type PermissionDefinition struct { // name is optional. Used to override "Type" for function naming. - name string - // Type should be a unique string to identify the - Type string + Name string // Actions are a map of actions to some description of what the action // should represent. The key in the actions map is the verb to use // in the rbac policy. Actions map[Action]ActionDefinition } -func (p PermissionDefinition) Name() string { - if p.name != "" { - return p.name - } - return p.Type -} - type ActionDefinition struct { // Human friendly description to explain the action. Description string @@ -73,6 +64,9 @@ func actDef(fields actionFields, description string) ActionDefinition { func (a ActionDefinition) Requires() string { fields := make([]string, 0) + if a.Fields&fieldID != 0 { + fields = append(fields, "uuid") + } if a.Fields&fieldOwner != 0 { fields = append(fields, "owner") } @@ -86,10 +80,10 @@ func (a ActionDefinition) Requires() string { return strings.Join(fields, ",") } -var RBACPermissions = []PermissionDefinition{ - { - name: "Wildcard", - Type: WildcardSymbol, +// RBACPermissions is indexed by the type +var RBACPermissions = map[string]PermissionDefinition{ + WildcardSymbol: { + Name: "Wildcard", Actions: map[Action]ActionDefinition{ WildcardSymbol: { Description: "Wildcard gives admin level access to all resources and all actions.", @@ -97,8 +91,7 @@ var RBACPermissions = []PermissionDefinition{ }, }, }, - { - Type: "user", + "user": { Actions: map[Action]ActionDefinition{ // Actions deal with site wide user objects. ActionRead: actDef(0, "read user data"), @@ -111,8 +104,7 @@ var RBACPermissions = []PermissionDefinition{ //ActionReadPublic: actDef(fieldOwner, "read public user data"), }, }, - { - Type: "workspace", + "workspace": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(fieldOwner|fieldOrg, "create a new workspace"), ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data to view on the UI"), @@ -131,12 +123,10 @@ var RBACPermissions = []PermissionDefinition{ ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"), }, }, - { - Type: "workspace_dormant", + "workspace_dormant": { Actions: map[Action]ActionDefinition{}, }, - { - Type: "workspace_proxy", + "workspace_proxy": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, "create a workspace proxy"), ActionDelete: actDef(0, "delete a workspace proxy"), @@ -144,8 +134,7 @@ var RBACPermissions = []PermissionDefinition{ ActionRead: actDef(0, "read and use a workspace proxy"), }, }, - { - Type: "license", + "license": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, "create a license"), ActionRead: actDef(0, "read licenses"), @@ -153,32 +142,27 @@ var RBACPermissions = []PermissionDefinition{ // Licenses are immutable, so update makes no sense }, }, - { - Type: "audit_log", + "audit_log": { Actions: map[Action]ActionDefinition{ ActionRead: actDef(0, "read audit logs"), }, }, - { - Type: "deployment_config", + "deployment_config": { Actions: map[Action]ActionDefinition{ ActionRead: actDef(0, "read deployment config"), }, }, - { - Type: "deployment_stats", + "deployment_stats": { Actions: map[Action]ActionDefinition{ ActionRead: actDef(0, "read deployment stats"), }, }, - { - Type: "replicas", + "replicas": { Actions: map[Action]ActionDefinition{ ActionRead: actDef(0, "read replicas"), }, }, - { - Type: "template", + "template": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(fieldOrg, "create a template"), // TODO: Create a use permission maybe? @@ -188,8 +172,7 @@ var RBACPermissions = []PermissionDefinition{ ActionViewInsights: actDef(fieldOrg|fieldACL, "view insights"), }, }, - { - Type: "group", + "group": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(fieldOrg, "create a group"), ActionRead: actDef(fieldOrg, "read groups"), @@ -197,15 +180,13 @@ var RBACPermissions = []PermissionDefinition{ ActionUpdate: actDef(fieldOrg, "update a group"), }, }, - { - Type: "file", + "file": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, "create a file"), ActionRead: actDef(0, "read files"), }, }, - { - Type: "provisioner_daemon", + "provisioner_daemon": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(fieldOrg, "create a provisioner daemon"), // TODO: Move to use? @@ -214,16 +195,14 @@ var RBACPermissions = []PermissionDefinition{ ActionDelete: actDef(fieldOrg, "delete a provisioner daemon"), }, }, - { - Type: "organization", + "organization": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, "create an organization"), ActionRead: actDef(0, "read organizations"), ActionDelete: actDef(0, "delete a organization"), }, }, - { - Type: "organization_member", + "organization_member": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(fieldOrg, "create an organization member"), ActionRead: actDef(fieldOrg, "read member"), @@ -231,14 +210,12 @@ var RBACPermissions = []PermissionDefinition{ ActionDelete: actDef(fieldOrg, "delete member"), }, }, - { - Type: "debug_info", + "debug_info": { Actions: map[Action]ActionDefinition{ ActionUse: actDef(0, "access to debug routes"), }, }, - { - Type: "system", + "system": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, "create system resources"), ActionRead: actDef(0, "view system resources"), @@ -246,16 +223,14 @@ var RBACPermissions = []PermissionDefinition{ ActionDelete: actDef(0, "delete system resources"), }, }, - { - Type: "api_key", + "api_key": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(fieldOwner, "create an api key"), ActionRead: actDef(fieldOwner, "read api key details (secrets are not stored)"), ActionDelete: actDef(fieldOwner, "delete an api key"), }, }, - { - Type: "tailnet_coordinator", + "tailnet_coordinator": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, ""), ActionRead: actDef(0, ""), @@ -263,23 +238,20 @@ var RBACPermissions = []PermissionDefinition{ ActionDelete: actDef(0, ""), }, }, - { - Type: "assign_role", + "assign_role": { Actions: map[Action]ActionDefinition{ ActionAssign: actDef(0, "ability to assign roles"), ActionRead: actDef(0, "view what roles are assignable"), ActionDelete: actDef(0, "ability to delete roles"), }, }, - { - Type: "assign_org_role", + "assign_org_role": { Actions: map[Action]ActionDefinition{ ActionAssign: actDef(0, "ability to assign org scoped roles"), ActionDelete: actDef(0, "ability to delete org scoped roles"), }, }, - { - Type: "oauth2_app", + "oauth2_app": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, "make an OAuth2 app."), ActionRead: actDef(0, "read OAuth2 apps"), @@ -287,8 +259,7 @@ var RBACPermissions = []PermissionDefinition{ ActionDelete: actDef(0, "delete an OAuth2 app"), }, }, - { - Type: "oauth2_app_secret", + "oauth2_app_secret": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, ""), ActionRead: actDef(0, ""), @@ -296,8 +267,7 @@ var RBACPermissions = []PermissionDefinition{ ActionDelete: actDef(0, ""), }, }, - { - Type: "oauth2_app_code_token", + "oauth2_app_code_token": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, ""), ActionRead: actDef(0, ""), diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 12b90782a5e23..6c67cec54af68 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -6,25 +6,18 @@ import ( _ "embed" "fmt" "go/format" - "go/types" "html/template" "log" "os" - "sort" + "slices" "strings" - "golang.org/x/tools/go/packages" - "github.com/coder/coder/v2/coderd/rbac/policy" ) //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. @@ -32,7 +25,7 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - out := gen2(ctx) + out := generate(ctx) formatted, err := format.Source(out) if err != nil { log.Fatalf("Format template: %s", err.Error()) @@ -40,52 +33,6 @@ func main() { _, _ = fmt.Fprint(os.Stdout, string(formatted)) return - - //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 pascalCaseName(name string) string { @@ -100,7 +47,19 @@ func capitalize(name string) string { return strings.ToUpper(string(name[0])) + name[1:] } -func gen2(ctx context.Context) []byte { +type Definition struct { + policy.PermissionDefinition + Type string +} + +func (p Definition) FunctionName() string { + if p.Name != "" { + return p.Name + } + return p.Type +} + +func generate(ctx context.Context) []byte { tpl, err := template.New("object.gotmpl").Funcs(template.FuncMap{ "capitalize": capitalize, "pascalCaseName": pascalCaseName, @@ -110,23 +69,22 @@ func gen2(ctx context.Context) []byte { } var out bytes.Buffer - err = tpl.Execute(&out, policy.RBACPermissions) + list := make([]Definition, 0) + for t, v := range policy.RBACPermissions { + v := v + list = append(list, Definition{ + PermissionDefinition: v, + Type: t, + }) + } + slices.SortFunc(list, func(a, b Definition) int { + return strings.Compare(a.Type, b.Type) + }) + + err = tpl.Execute(&out, list) if err != nil { log.Fatalf("Execute template: %s", err.Error()) } return out.Bytes() } - -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/v2/coderd/rbac.Object" { - resources = append(resources, obj.Name()) - } - } - sort.Strings(resources) - return resources -} diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/object.gotmpl index 7e99392bc8b91..d4e7467178f5a 100644 --- a/scripts/rbacgen/object.gotmpl +++ b/scripts/rbacgen/object.gotmpl @@ -4,12 +4,13 @@ package rbac var ( {{- range $element := . }} - // Resource{{ pascalCaseName $element.Name }} + {{- $Name := pascalCaseName $element.FunctionName }} + // Resource{{ $Name }} // Valid Actions {{- range $action, $value := .Actions }} // - "{{ $action }}" needs [{{ $value.Requires }}] :: {{ $value.Description }} {{- end }} - Resource{{ pascalCaseName $element.Name }} = Object { + Resource{{ $Name }} = Object { Type: "{{ $element.Type }}", } {{ end -}} @@ -18,7 +19,7 @@ var ( func AllResources() []Object { return []Object{ {{- range $element := . }} - Resource{{ pascalCaseName $element.Name }}, + Resource{{ pascalCaseName $element.FunctionName }}, {{- end }} } } From 22861a1dc9ec4ecd0ee5abc57bd996ea15f24ccc Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 13 May 2024 16:06:00 -0500 Subject: [PATCH 07/24] fix some compile issus --- coderd/rbac/object.go | 14 +++++++++++++- coderd/rbac/policy/policy.go | 11 ++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index dbd873917bc27..6a44d79451843 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -37,8 +37,20 @@ type Object struct { ACLGroupList map[string][]policy.Action ` json:"acl_group_list"` } +// AvailableActions returns all available actions for a given object. +// Wildcard is omitted. func (z Object) AvailableActions() []policy.Action { - policy.Action() + perms, ok := policy.RBACPermissions[z.Type] + if !ok { + return []policy.Action{} + } + + actions := make([]policy.Action, 0, len(perms.Actions)) + for action := range perms.Actions { + actions = append(actions, action) + } + + return actions } func (z Object) Equal(b Object) bool { diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index bee869afa7daa..cfa3499990f8f 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -82,14 +82,11 @@ func (a ActionDefinition) Requires() string { // RBACPermissions is indexed by the type var RBACPermissions = map[string]PermissionDefinition{ + // Wildcard is every object, and the action "*" provides all actions. + // So can grant all actions on all types. WildcardSymbol: { - Name: "Wildcard", - Actions: map[Action]ActionDefinition{ - WildcardSymbol: { - Description: "Wildcard gives admin level access to all resources and all actions.", - Fields: 0, - }, - }, + Name: "Wildcard", + Actions: map[Action]ActionDefinition{}, }, "user": { Actions: map[Action]ActionDefinition{ From 1b7112c504a5a7945456ce8a7d9ec5a48d3552e4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 13 May 2024 23:51:28 -0500 Subject: [PATCH 08/24] begin some aciton work --- coderd/database/dbauthz/dbauthz.go | 1 + coderd/rbac/object.go | 5 - coderd/rbac/object_gen.go | 314 ++++++++++++++++++++++++----- coderd/rbac/policy/policy.go | 7 +- coderd/rbac/roles.go | 100 +++++---- coderd/users.go | 2 +- coderd/util/slice/slice.go | 12 ++ coderd/util/slice/slice_test.go | 8 + scripts/rbacgen/main.go | 87 +++++++- scripts/rbacgen/object.gotmpl | 25 ++- 10 files changed, 444 insertions(+), 117 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 3d9129928c811..6674f4e1859a4 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -16,6 +16,7 @@ import ( "github.com/open-policy-agent/opa/topdown" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 6a44d79451843..1f4c3545ca618 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -8,11 +8,6 @@ import ( const WildcardSymbol = "*" -// Objecter returns the RBAC object for itself. -type Objecter interface { - RBACObject() Object -} - // ResourceUserObject is a helper function to create a user object for authz checks. func ResourceUserObject(userID uuid.UUID) Object { return ResourceUser.WithID(userID).WithOwner(userID.String()) diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index facb7472e58c3..94abe424419a9 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -1,12 +1,20 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac +import "github.com/coder/coder/v2/coderd/rbac/policy" + +// Objecter returns the RBAC object for itself. +type Objecter interface { + RBACObject() Object +} + var ( // ResourceWildcard // Valid Actions - // - "*" needs [] :: Wildcard gives admin level access to all resources and all actions. - ResourceWildcard = Object{ - Type: "*", + ResourceWildcard = WildcardObject{ + Object: Object{ + Type: "*", + }, } // ResourceApiKey @@ -14,16 +22,20 @@ var ( // - "create" needs [owner] :: create an api key // - "delete" needs [owner] :: delete an api key // - "read" needs [owner] :: read api key details (secrets are not stored) - ResourceApiKey = Object{ - Type: "api_key", + ResourceApiKey = ApiKeyObject{ + Object: Object{ + Type: "api_key", + }, } // ResourceAssignOrgRole // Valid Actions // - "assign" needs [] :: ability to assign org scoped roles // - "delete" needs [] :: ability to delete org scoped roles - ResourceAssignOrgRole = Object{ - Type: "assign_org_role", + ResourceAssignOrgRole = AssignOrgRoleObject{ + Object: Object{ + Type: "assign_org_role", + }, } // ResourceAssignRole @@ -31,44 +43,56 @@ var ( // - "assign" needs [] :: ability to assign roles // - "delete" needs [] :: ability to delete roles // - "read" needs [] :: view what roles are assignable - ResourceAssignRole = Object{ - Type: "assign_role", + ResourceAssignRole = AssignRoleObject{ + Object: Object{ + Type: "assign_role", + }, } // ResourceAuditLog // Valid Actions // - "read" needs [] :: read audit logs - ResourceAuditLog = Object{ - Type: "audit_log", + ResourceAuditLog = AuditLogObject{ + Object: Object{ + Type: "audit_log", + }, } // ResourceDebugInfo // Valid Actions // - "use" needs [] :: access to debug routes - ResourceDebugInfo = Object{ - Type: "debug_info", + ResourceDebugInfo = DebugInfoObject{ + Object: Object{ + Type: "debug_info", + }, } // ResourceDeploymentConfig // Valid Actions // - "read" needs [] :: read deployment config - ResourceDeploymentConfig = Object{ - Type: "deployment_config", + ResourceDeploymentConfig = DeploymentConfigObject{ + Object: Object{ + Type: "deployment_config", + }, } // ResourceDeploymentStats // Valid Actions // - "read" needs [] :: read deployment stats - ResourceDeploymentStats = Object{ - Type: "deployment_stats", + ResourceDeploymentStats = DeploymentStatsObject{ + Object: Object{ + Type: "deployment_stats", + }, } // ResourceFile // Valid Actions // - "create" needs [] :: create a file // - "read" needs [] :: read files - ResourceFile = Object{ - Type: "file", + ResourceFile = FileObject{ + Object: Object{ + Type: "file", + }, } // ResourceGroup @@ -77,8 +101,10 @@ var ( // - "delete" needs [org] :: delete a group // - "read" needs [org] :: read groups // - "update" needs [org] :: update a group - ResourceGroup = Object{ - Type: "group", + ResourceGroup = GroupObject{ + Object: Object{ + Type: "group", + }, } // ResourceLicense @@ -86,8 +112,10 @@ var ( // - "create" needs [] :: create a license // - "delete" needs [] :: delete license // - "read" needs [] :: read licenses - ResourceLicense = Object{ - Type: "license", + ResourceLicense = LicenseObject{ + Object: Object{ + Type: "license", + }, } // ResourceOauth2App @@ -96,8 +124,10 @@ var ( // - "delete" needs [] :: delete an OAuth2 app // - "read" needs [] :: read OAuth2 apps // - "update" needs [] :: update the properties of the OAuth2 app. - ResourceOauth2App = Object{ - Type: "oauth2_app", + ResourceOauth2App = Oauth2AppObject{ + Object: Object{ + Type: "oauth2_app", + }, } // ResourceOauth2AppCodeToken @@ -105,8 +135,10 @@ var ( // - "create" needs [] :: // - "delete" needs [] :: // - "read" needs [] :: - ResourceOauth2AppCodeToken = Object{ - Type: "oauth2_app_code_token", + ResourceOauth2AppCodeToken = Oauth2AppCodeTokenObject{ + Object: Object{ + Type: "oauth2_app_code_token", + }, } // ResourceOauth2AppSecret @@ -115,8 +147,10 @@ var ( // - "delete" needs [] :: // - "read" needs [] :: // - "update" needs [] :: - ResourceOauth2AppSecret = Object{ - Type: "oauth2_app_secret", + ResourceOauth2AppSecret = Oauth2AppSecretObject{ + Object: Object{ + Type: "oauth2_app_secret", + }, } // ResourceOrganization @@ -124,8 +158,10 @@ var ( // - "create" needs [] :: create an organization // - "delete" needs [] :: delete a organization // - "read" needs [] :: read organizations - ResourceOrganization = Object{ - Type: "organization", + ResourceOrganization = OrganizationObject{ + Object: Object{ + Type: "organization", + }, } // ResourceOrganizationMember @@ -134,8 +170,10 @@ var ( // - "delete" needs [org] :: delete member // - "read" needs [org] :: read member // - "update" needs [org] :: update a organization member - ResourceOrganizationMember = Object{ - Type: "organization_member", + ResourceOrganizationMember = OrganizationMemberObject{ + Object: Object{ + Type: "organization_member", + }, } // ResourceProvisionerDaemon @@ -144,15 +182,19 @@ var ( // - "delete" needs [org] :: delete a provisioner daemon // - "read" needs [org] :: read provisioner daemon // - "update" needs [org] :: update a provisioner daemon - ResourceProvisionerDaemon = Object{ - Type: "provisioner_daemon", + ResourceProvisionerDaemon = ProvisionerDaemonObject{ + Object: Object{ + Type: "provisioner_daemon", + }, } // ResourceReplicas // Valid Actions // - "read" needs [] :: read replicas - ResourceReplicas = Object{ - Type: "replicas", + ResourceReplicas = ReplicasObject{ + Object: Object{ + Type: "replicas", + }, } // ResourceSystem @@ -161,8 +203,10 @@ var ( // - "delete" needs [] :: delete system resources // - "read" needs [] :: view system resources // - "update" needs [] :: update system resources - ResourceSystem = Object{ - Type: "system", + ResourceSystem = SystemObject{ + Object: Object{ + Type: "system", + }, } // ResourceTailnetCoordinator @@ -171,8 +215,10 @@ var ( // - "delete" needs [] :: // - "read" needs [] :: // - "update" needs [] :: - ResourceTailnetCoordinator = Object{ - Type: "tailnet_coordinator", + ResourceTailnetCoordinator = TailnetCoordinatorObject{ + Object: Object{ + Type: "tailnet_coordinator", + }, } // ResourceTemplate @@ -182,8 +228,10 @@ var ( // - "read" needs [org,acl] :: read template // - "update" needs [org,acl] :: update a template // - "view_insights" needs [org,acl] :: view insights - ResourceTemplate = Object{ - Type: "template", + ResourceTemplate = TemplateObject{ + Object: Object{ + Type: "template", + }, } // ResourceUser @@ -194,8 +242,10 @@ var ( // - "read_personal" needs [owner] :: read personal user data like password // - "update" needs [] :: update an existing user // - "update_personal" needs [owner] :: update personal data - ResourceUser = Object{ - Type: "user", + ResourceUser = UserObject{ + Object: Object{ + Type: "user", + }, } // ResourceWorkspace @@ -208,14 +258,18 @@ var ( // - "read" needs [owner,org,acl] :: read workspace data to view on the UI // - "ssh" needs [owner,org,acl] :: ssh into a given workspace // - "update" needs [owner,org,acl] :: edit workspace settings (scheduling, permissions, parameters) - ResourceWorkspace = Object{ - Type: "workspace", + ResourceWorkspace = WorkspaceObject{ + Object: Object{ + Type: "workspace", + }, } // ResourceWorkspaceDormant // Valid Actions - ResourceWorkspaceDormant = Object{ - Type: "workspace_dormant", + ResourceWorkspaceDormant = WorkspaceDormantObject{ + Object: Object{ + Type: "workspace_dormant", + }, } // ResourceWorkspaceProxy @@ -224,13 +278,15 @@ var ( // - "delete" needs [] :: delete a workspace proxy // - "read" needs [] :: read and use a workspace proxy // - "update" needs [] :: update a workspace proxy - ResourceWorkspaceProxy = Object{ - Type: "workspace_proxy", + ResourceWorkspaceProxy = WorkspaceProxyObject{ + Object: Object{ + Type: "workspace_proxy", + }, } ) -func AllResources() []Object { - return []Object{ +func AllResources() []Objecter { + return []Objecter{ ResourceWildcard, ResourceApiKey, ResourceAssignOrgRole, @@ -258,3 +314,155 @@ func AllResources() []Object { ResourceWorkspaceProxy, } } + +type WildcardObject struct{ Object } + +type ApiKeyObject struct{ Object } + +func (ApiKeyObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (ApiKeyObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (ApiKeyObject) ActionRead() policy.Action { return policy.ActionRead } + +type AssignOrgRoleObject struct{ Object } + +func (AssignOrgRoleObject) ActionAssign() policy.Action { return policy.ActionAssign } +func (AssignOrgRoleObject) ActionDelete() policy.Action { return policy.ActionDelete } + +type AssignRoleObject struct{ Object } + +func (AssignRoleObject) ActionAssign() policy.Action { return policy.ActionAssign } +func (AssignRoleObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (AssignRoleObject) ActionRead() policy.Action { return policy.ActionRead } + +type AuditLogObject struct{ Object } + +func (AuditLogObject) ActionRead() policy.Action { return policy.ActionRead } + +type DebugInfoObject struct{ Object } + +func (DebugInfoObject) ActionUse() policy.Action { return policy.ActionUse } + +type DeploymentConfigObject struct{ Object } + +func (DeploymentConfigObject) ActionRead() policy.Action { return policy.ActionRead } + +type DeploymentStatsObject struct{ Object } + +func (DeploymentStatsObject) ActionRead() policy.Action { return policy.ActionRead } + +type FileObject struct{ Object } + +func (FileObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (FileObject) ActionRead() policy.Action { return policy.ActionRead } + +type GroupObject struct{ Object } + +func (GroupObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (GroupObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (GroupObject) ActionRead() policy.Action { return policy.ActionRead } +func (GroupObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type LicenseObject struct{ Object } + +func (LicenseObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (LicenseObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (LicenseObject) ActionRead() policy.Action { return policy.ActionRead } + +type Oauth2AppObject struct{ Object } + +func (Oauth2AppObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (Oauth2AppObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (Oauth2AppObject) ActionRead() policy.Action { return policy.ActionRead } +func (Oauth2AppObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type Oauth2AppCodeTokenObject struct{ Object } + +func (Oauth2AppCodeTokenObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (Oauth2AppCodeTokenObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (Oauth2AppCodeTokenObject) ActionRead() policy.Action { return policy.ActionRead } + +type Oauth2AppSecretObject struct{ Object } + +func (Oauth2AppSecretObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (Oauth2AppSecretObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (Oauth2AppSecretObject) ActionRead() policy.Action { return policy.ActionRead } +func (Oauth2AppSecretObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type OrganizationObject struct{ Object } + +func (OrganizationObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (OrganizationObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (OrganizationObject) ActionRead() policy.Action { return policy.ActionRead } + +type OrganizationMemberObject struct{ Object } + +func (OrganizationMemberObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (OrganizationMemberObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (OrganizationMemberObject) ActionRead() policy.Action { return policy.ActionRead } +func (OrganizationMemberObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type ProvisionerDaemonObject struct{ Object } + +func (ProvisionerDaemonObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (ProvisionerDaemonObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (ProvisionerDaemonObject) ActionRead() policy.Action { return policy.ActionRead } +func (ProvisionerDaemonObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type ReplicasObject struct{ Object } + +func (ReplicasObject) ActionRead() policy.Action { return policy.ActionRead } + +type SystemObject struct{ Object } + +func (SystemObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (SystemObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (SystemObject) ActionRead() policy.Action { return policy.ActionRead } +func (SystemObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type TailnetCoordinatorObject struct{ Object } + +func (TailnetCoordinatorObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (TailnetCoordinatorObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (TailnetCoordinatorObject) ActionRead() policy.Action { return policy.ActionRead } +func (TailnetCoordinatorObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type TemplateObject struct{ Object } + +func (TemplateObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (TemplateObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (TemplateObject) ActionRead() policy.Action { return policy.ActionRead } +func (TemplateObject) ActionUpdate() policy.Action { return policy.ActionUpdate } +func (TemplateObject) ActionViewInsights() policy.Action { return policy.ActionViewInsights } + +type UserObject struct{ Object } + +func (UserObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (UserObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (UserObject) ActionRead() policy.Action { return policy.ActionRead } +func (UserObject) ActionReadPersonal() policy.Action { return policy.ActionReadPersonal } +func (UserObject) ActionUpdate() policy.Action { return policy.ActionUpdate } +func (UserObject) ActionUpdatePersonal() policy.Action { return policy.ActionUpdatePersonal } + +type WorkspaceObject struct{ Object } + +func (WorkspaceObject) ActionApplicationConnect() policy.Action { + return policy.ActionApplicationConnect +} +func (WorkspaceObject) ActionWorkspaceBuild() policy.Action { return policy.ActionWorkspaceBuild } +func (WorkspaceObject) ActionViewWorkspaceBuildParams() policy.Action { + return policy.ActionViewWorkspaceBuildParams +} +func (WorkspaceObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (WorkspaceObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (WorkspaceObject) ActionRead() policy.Action { return policy.ActionRead } +func (WorkspaceObject) ActionSSH() policy.Action { return policy.ActionSSH } +func (WorkspaceObject) ActionUpdate() policy.Action { return policy.ActionUpdate } + +type WorkspaceDormantObject struct{ Object } + +type WorkspaceProxyObject struct{ Object } + +func (WorkspaceProxyObject) ActionCreate() policy.Action { return policy.ActionCreate } +func (WorkspaceProxyObject) ActionDelete() policy.Action { return policy.ActionDelete } +func (WorkspaceProxyObject) ActionRead() policy.Action { return policy.ActionRead } +func (WorkspaceProxyObject) ActionUpdate() policy.Action { return policy.ActionUpdate } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index cfa3499990f8f..3e79253e83e82 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -24,6 +24,9 @@ const ( ActionViewWorkspaceBuildParams Action = "build_parameters" ActionAssign Action = "assign" + + ActionReadPersonal Action = "read_personal" + ActionUpdatePersonal Action = "update_personal" ) const ( @@ -96,8 +99,8 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionUpdate: actDef(0, "update an existing user"), ActionDelete: actDef(0, "delete an existing user"), - "read_personal": actDef(fieldOwner, "read personal user data like password"), - "update_personal": actDef(fieldOwner, "update personal data"), + ActionReadPersonal: actDef(fieldOwner, "read personal user data like password"), + ActionUpdatePersonal: actDef(fieldOwner, "update personal data"), //ActionReadPublic: actDef(fieldOwner, "read public user data"), }, }, diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index f69cf49174f60..5ebd2b7f66df8 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -10,6 +10,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/util/slice" ) const ( @@ -70,27 +71,27 @@ func RoleOrgMember(organizationID uuid.UUID) string { return roleName(orgMember, organizationID.String()) } -func allPermsExcept(excepts ...Object) []Permission { +func allPermsExcept(excepts ...Objecter) []Permission { resources := AllResources() var perms []Permission skip := make(map[string]bool) for _, e := range excepts { - skip[e.Type] = true + skip[e.RBACObject().Type] = true } for _, r := range resources { // Exceptions - if skip[r.Type] { + if skip[r.RBACObject().Type] { continue } // This should always be skipped. - if r.Type == ResourceWildcard.Type { + if r.RBACObject().Type == ResourceWildcard.Type { continue } // Owners can do everything else perms = append(perms, Permission{ Negate: false, - ResourceType: r.Type, + ResourceType: r.RBACObject().Type, Action: WildcardSymbol, }) } @@ -123,12 +124,12 @@ func ReloadBuiltinRoles(opts *RoleOptions) { opts = &RoleOptions{} } - ownerAndAdminExceptions := []Object{ResourceWorkspaceDormant} + ownerWorkspaceActions := ResourceWorkspace.AvailableActions() if opts.NoOwnerWorkspaceExec { - ownerAndAdminExceptions = append(ownerAndAdminExceptions, - ResourceWorkspaceExecution, - ResourceWorkspaceApplicationConnect, - ) + // Remove ssh and application connect from the owner role. This + // prevents owners from have exec access to all workspaces. + ownerWorkspaceActions = slice.Omit(ownerWorkspaceActions, + ResourceWorkspace.ActionApplicationConnect(), ResourceWorkspace.ActionSSH()) } // Static roles that never change should be allocated in a closure. @@ -138,20 +139,27 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ownerRole := Role{ Name: owner, DisplayName: "Owner", - Site: allPermsExcept(ownerAndAdminExceptions...), - Org: map[string][]Permission{}, - User: []Permission{}, + Site: append( + // Workspace dormancy and workspace are omitted. + // Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec + allPermsExcept(ResourceWorkspaceDormant, ResourceWorkspace), + // This adds back in the Workspace permissions. + Permissions(map[string][]policy.Action{ + ResourceWorkspace.Type: ownerWorkspaceActions, + })...), + Org: map[string][]Permission{}, + User: []Permission{}, }.withCachedRegoValue() memberRole := Role{ Name: member, DisplayName: "Member", Site: Permissions(map[string][]policy.Action{ - ResourceRoleAssignment.Type: {policy.ActionRead}, + ResourceRoleAssignment.Type: {ActionRead}, // All users can see the provisioner daemons. - ResourceProvisionerDaemon.Type: {policy.ActionRead}, + ResourceProvisionerDaemon.Type: {ResourceProvisionerDaemon.ActionRead()}, // All users can see OAuth2 provider applications. - ResourceOAuth2ProviderApp.Type: {policy.ActionRead}, + ResourceOAuth2ProviderApp.Type: {ActionRead}, }), Org: map[string][]Permission{}, User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember), @@ -159,9 +167,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // Users cannot do create/update/delete on themselves, but they // can read their own details. ResourceUser.Type: {policy.ActionRead}, - ResourceUserWorkspaceBuildParameters.Type: {policy.ActionRead}, + ResourceUserWorkspaceBuildParameters.Type: {ActionRead}, // Users can create provisioner daemons scoped to themselves. - ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, + ResourceProvisionerDaemon.Type: {ResourceProvisionerDaemon.ActionRead(), ActionCreate, ActionRead, ActionUpdate}, })..., ), }.withCachedRegoValue() @@ -172,16 +180,15 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Site: Permissions(map[string][]policy.Action{ // Should be able to read all template details, even in orgs they // are not in. - ResourceTemplate.Type: {policy.ActionRead}, - ResourceTemplateInsights.Type: {policy.ActionRead}, - ResourceAuditLog.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, + ResourceTemplate.Type: {policy.ActionRead, ResourceTemplate.ActionViewInsights()}, + ResourceAuditLog.Type: {policy.ActionRead}, + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, // Allow auditors to query deployment stats and insights. ResourceDeploymentStats.Type: {policy.ActionRead}, - ResourceDeploymentValues.Type: {policy.ActionRead}, + ResourceDeploymentConfig.Type: {policy.ActionRead}, // Org roles are not really used yet, so grant the perm at the site level. - ResourceOrganizationMember.Type: {policy.ActionRead}, + ResourceOrganizationMember.Type: {ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -191,20 +198,20 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Name: templateAdmin, DisplayName: "Template Admin", Site: Permissions(map[string][]policy.Action{ - ResourceTemplate.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceTemplate.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, // CRUD all files, even those they did not upload. - ResourceFile.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceWorkspace.Type: {policy.ActionRead}, + ResourceFile.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceWorkspace.Type: {ActionRead}, // CRUD to provisioner daemons for now. - ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, // Needs to read all organizations since - ResourceOrganization.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, + ResourceOrganization.Type: {ActionRead}, + ResourceUser.Type: {ActionRead}, + ResourceGroup.Type: {ActionRead}, // Org roles are not really used yet, so grant the perm at the site level. - ResourceOrganizationMember.Type: {policy.ActionRead}, + ResourceOrganizationMember.Type: {ActionRead}, // Template admins can read all template insights data - ResourceTemplateInsights.Type: {policy.ActionRead}, + ResourceTemplateInsights.Type: {ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -214,13 +221,13 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Name: userAdmin, DisplayName: "User Admin", Site: Permissions(map[string][]policy.Action{ - ResourceRoleAssignment.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceUser.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceUserData.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceUserWorkspaceBuildParameters.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceUserData.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceUserWorkspaceBuildParameters.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, // Full perms to manage org members - ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceGroup.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -279,19 +286,19 @@ func ReloadBuiltinRoles(opts *RoleOptions) { { // All org members can read the organization ResourceType: ResourceOrganization.Type, - Action: policy.ActionRead, + Action: ActionRead, }, { // Can read available roles. ResourceType: ResourceOrgRoleAssignment.Type, - Action: policy.ActionRead, + Action: ActionRead, }, }, }, User: []Permission{ { ResourceType: ResourceOrganizationMember.Type, - Action: policy.ActionRead, + Action: ActionRead, }, }, } @@ -523,7 +530,7 @@ func SiteRoles() []Role { // ChangeRoleSet is a helper function that finds the difference of 2 sets of // roles. When setting a user's new roles, it is equivalent to adding and // removing roles. This set determines the changes, so that the appropriate -// RBAC checks can be applied using "policy.ActionCreate" and "policy.ActionDelete" for +// RBAC checks can be applied using "ActionCreate" and "ActionDelete" for // "added" and "removed" roles respectively. func ChangeRoleSet(from []string, to []string) (added []string, removed []string) { has := make(map[string]struct{}) @@ -579,6 +586,11 @@ func roleSplit(role string) (name string, orgID string, err error) { return arr[0], "", nil } +func Perm[T Objecter](f func(o T) []policy.Action) []policy.Action { + var t T + return f(t) +} + // Permissions is just a helper function to make building roles that list out resources // and actions a bit easier. func Permissions(perms map[string][]policy.Action) []Permission { diff --git a/coderd/users.go b/coderd/users.go index c698661d71429..f24abda349c40 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1022,7 +1022,7 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() user := httpmw.UserParam(r) - if !api.Authorize(r, policy.ActionRead, user.UserDataRBACObject()) { + if !api.Authorize(r, policy.ActionRead, user) { httpapi.ResourceNotFound(rw) return } diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 8586aae770610..f06930f373557 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -4,6 +4,18 @@ import ( "golang.org/x/exp/constraints" ) +// Omit creates a new slice with the arguments omitted from the list. +func Omit[T comparable](a []T, omits ...T) []T { + tmp := make([]T, 0, len(a)) + for _, v := range a { + if Contains(omits, v) { + continue + } + tmp = append(tmp, v) + } + return tmp +} + // SameElements returns true if the 2 lists have the same elements in any // order. func SameElements[T comparable](a []T, b []T) bool { diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go index cf686f3de4a48..ef947a13e7659 100644 --- a/coderd/util/slice/slice_test.go +++ b/coderd/util/slice/slice_test.go @@ -123,3 +123,11 @@ func TestDescending(t *testing.T) { assert.Equal(t, 0, slice.Descending(1, 1)) assert.Equal(t, -1, slice.Descending(2, 1)) } + +func TestOmit(t *testing.T) { + t.Parallel() + + assert.Equal(t, []string{"a", "b", "f"}, + slice.Omit([]string{"a", "b", "c", "d", "e", "f"}, "c", "d", "e"), + ) +} diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 6c67cec54af68..6affc14e900cd 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -4,8 +4,12 @@ import ( "bytes" "context" _ "embed" + "errors" "fmt" + "go/ast" "go/format" + "go/parser" + "go/token" "html/template" "log" "os" @@ -25,7 +29,11 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - out := generate(ctx) + out, err := generate(ctx) + if err != nil { + log.Fatalf("Generate source: %s", err.Error()) + } + formatted, err := format.Source(out) if err != nil { log.Fatalf("Format template: %s", err.Error()) @@ -35,8 +43,8 @@ func main() { return } -func pascalCaseName(name string) string { - names := strings.Split(name, "_") +func pascalCaseName[T ~string](name T) string { + names := strings.Split(string(name), "_") for i := range names { names[i] = capitalize(names[i]) } @@ -59,13 +67,72 @@ func (p Definition) FunctionName() string { return p.Type } -func generate(ctx context.Context) []byte { +func fileActions(file *ast.File) map[string]string { + // actions is a map from the enum value -> enum name + actions := make(map[string]string) + + // Find the action consts +fileDeclLoop: + for _, decl := range file.Decls { + switch typedDecl := decl.(type) { + case *ast.GenDecl: + if len(typedDecl.Specs) == 0 { + continue + } + // This is the right on, loop over all idents, pull the actions + for _, spec := range typedDecl.Specs { + vSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue fileDeclLoop + } + + typeIdent, ok := vSpec.Type.(*ast.Ident) + if !ok { + continue fileDeclLoop + } + + if typeIdent.Name != "Action" || len(vSpec.Values) != 1 || len(vSpec.Names) != 1 { + continue fileDeclLoop + } + + literal, ok := vSpec.Values[0].(*ast.BasicLit) + if !ok { + continue fileDeclLoop + } + actions[strings.Trim(literal.Value, `"`)] = vSpec.Names[0].Name + } + default: + continue + } + } + return actions +} + +func generate(ctx context.Context) ([]byte, error) { + // Parse the policy.go file for the action enums + f, err := parser.ParseFile(token.NewFileSet(), "./coderd/rbac/policy/policy.go", nil, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("parsing policy.go: %w", err) + } + actionMap := fileActions(f) + + var errorList []error + var x int tpl, err := template.New("object.gotmpl").Funcs(template.FuncMap{ "capitalize": capitalize, - "pascalCaseName": pascalCaseName, + "pascalCaseName": pascalCaseName[string], + "actionEnum": func(action policy.Action) string { + x++ + v, ok := actionMap[string(action)] + if !ok { + errorList = append(errorList, fmt.Errorf("action value %q does not have a constant a matching enum constant", action)) + } + return v + }, + "concat": func(strs ...string) string { return strings.Join(strs, "") }, }).Parse(objectGoTpl) if err != nil { - log.Fatalf("Failed to parse templates: %s", err.Error()) + return nil, fmt.Errorf("parse template: %w", err) } var out bytes.Buffer @@ -83,8 +150,12 @@ func generate(ctx context.Context) []byte { err = tpl.Execute(&out, list) if err != nil { - log.Fatalf("Execute template: %s", err.Error()) + return nil, fmt.Errorf("execute template: %w", err) + } + + if len(errorList) > 0 { + return nil, errors.Join(errorList...) } - return out.Bytes() + return out.Bytes(), nil } diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/object.gotmpl index d4e7467178f5a..8feb9a4a3cb21 100644 --- a/scripts/rbacgen/object.gotmpl +++ b/scripts/rbacgen/object.gotmpl @@ -1,6 +1,12 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac +import "github.com/coder/coder/v2/coderd/rbac/policy" + +// Objecter returns the RBAC object for itself. +type Objecter interface { + RBACObject() Object +} var ( {{- range $element := . }} @@ -10,16 +16,27 @@ var ( {{- range $action, $value := .Actions }} // - "{{ $action }}" needs [{{ $value.Requires }}] :: {{ $value.Description }} {{- end }} - Resource{{ $Name }} = Object { - Type: "{{ $element.Type }}", + Resource{{ $Name }} = {{ $Name }}Object { + Object: Object{ + Type: "{{ $element.Type }}", + }, } {{ end -}} ) -func AllResources() []Object { - return []Object{ +func AllResources() []Objecter { + return []Objecter{ {{- range $element := . }} Resource{{ pascalCaseName $element.FunctionName }}, {{- end }} } } + +{{- range $element := . }} + {{- $Name := pascalCaseName $element.FunctionName }} + type {{ $Name }}Object struct{ Object } + {{- range $action, $value := .Actions -}} + {{ $ActionEnum := actionEnum $action }} + func ({{ $Name }}Object ) {{ $ActionEnum }}() policy.Action { return policy.{{ $ActionEnum }} } + {{- end }} +{{ end -}} From cd68fefb1e4d805e095b6124589da783f8fc051e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 13:02:57 -0500 Subject: [PATCH 09/24] more progrss --- coderd/authorize.go | 6 - coderd/database/dbauthz/dbauthz.go | 47 +-- coderd/database/modelmethods.go | 29 +- coderd/rbac/authz_internal_test.go | 164 +++++------ coderd/rbac/object.go | 15 + coderd/rbac/object_gen.go | 454 ++++++++--------------------- coderd/rbac/roles.go | 62 ++-- coderd/workspaceagents.go | 2 +- coderd/workspaceapps/db.go | 10 +- enterprise/coderd/templates.go | 2 +- scripts/rbacgen/object.gotmpl | 19 +- 11 files changed, 299 insertions(+), 511 deletions(-) diff --git a/coderd/authorize.go b/coderd/authorize.go index 9adff89769805..092200921ac5f 100644 --- a/coderd/authorize.go +++ b/coderd/authorize.go @@ -190,12 +190,6 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) { var dbErr error // Only support referencing some resources by ID. switch v.Object.ResourceType.String() { - case rbac.ResourceWorkspaceExecution.Type: - workSpace, err := api.Database.GetWorkspaceByID(ctx, id) - if err == nil { - dbObj = workSpace.ExecutionRBAC() - } - dbErr = err case rbac.ResourceWorkspace.Type: dbObj, dbErr = api.Database.GetWorkspaceByID(ctx, id) case rbac.ResourceTemplate.Type: diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 6674f4e1859a4..10eaf2a1c4ba4 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -193,11 +193,10 @@ var ( Name: "autostart", DisplayName: "Autostart Daemon", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, - rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, - rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate}, - rbac.ResourceWorkspaceBuild.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - rbac.ResourceUser.Type: {policy.ActionRead}, + rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, + rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, + rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceBuild}, + rbac.ResourceUser.Type: {policy.ActionRead}, }), Org: map[string][]rbac.Permission{}, User: []rbac.Permission{}, @@ -316,6 +315,20 @@ func insert[ authorizer rbac.Authorizer, object rbac.Objecter, insertFunc Insert, +) Insert { + return insertWithAction(logger, authorizer, object, policy.ActionCreate, insertFunc) +} + +func insertWithAction[ + ObjectType any, + ArgumentType any, + Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), +]( + logger slog.Logger, + authorizer rbac.Authorizer, + object rbac.Objecter, + action policy.Action, + insertFunc Insert, ) Insert { return func(ctx context.Context, arg ArgumentType) (empty ObjectType, err error) { // Fetch the rbac subject @@ -325,7 +338,7 @@ func insert[ } // Authorize the action - err = authorizer.Authorize(ctx, act, policy.ActionCreate, object.RBACObject()) + err = authorizer.Authorize(ctx, act, action, object.RBACObject()) if err != nil { return empty, logNotAuthorizedError(ctx, logger, err) } @@ -1804,19 +1817,19 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License, func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) { // Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { for _, templateID := range arg.TemplateIDs { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { return nil, err } - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, template.RBACObject()); err != nil { return nil, err } } if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { return nil, err } } @@ -1841,19 +1854,19 @@ func (q *querier) GetUserCount(ctx context.Context) (int64, error) { func (q *querier) GetUserLatencyInsights(ctx context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) { // Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { for _, templateID := range arg.TemplateIDs { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { return nil, err } - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { return nil, err } } if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { return nil, err } } @@ -2313,7 +2326,7 @@ func (q *querier) InsertDeploymentID(ctx context.Context, value string) error { } func (q *querier) InsertExternalAuthLink(ctx context.Context, arg database.InsertExternalAuthLinkParams) (database.ExternalAuthLink, error) { - return insert(q.log, q.auth, rbac.ResourceUserData.WithOwner(arg.UserID.String()).WithID(arg.UserID), q.db.InsertExternalAuthLink)(ctx, arg) + return insertWithAction(q.log, q.auth, rbac.ResourceUser.WithID(arg.UserID).WithOwner(arg.UserID.String()), policy.ActionUpdatePersonal, q.db.InsertExternalAuthLink)(ctx, arg) } func (q *querier) InsertFile(ctx context.Context, arg database.InsertFileParams) (database.File, error) { @@ -2321,7 +2334,7 @@ func (q *querier) InsertFile(ctx context.Context, arg database.InsertFileParams) } func (q *querier) InsertGitSSHKey(ctx context.Context, arg database.InsertGitSSHKeyParams) (database.GitSSHKey, error) { - return insert(q.log, q.auth, rbac.ResourceUserData.WithOwner(arg.UserID.String()).WithID(arg.UserID), q.db.InsertGitSSHKey)(ctx, arg) + return insertWithAction(q.log, q.auth, rbac.ResourceUser.WithOwner(arg.UserID.String()).WithID(arg.UserID), policy.ActionUpdatePersonal, q.db.InsertGitSSHKey)(ctx, arg) } func (q *querier) InsertGroup(ctx context.Context, arg database.InsertGroupParams) (database.Group, error) { @@ -2997,7 +3010,7 @@ func (q *querier) UpdateUserAppearanceSettings(ctx context.Context, arg database if err != nil { return database.User{}, err } - if err := q.authorizeContext(ctx, policy.ActionUpdate, u.UserDataRBACObject()); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.User{}, err } return q.db.UpdateUserAppearanceSettings(ctx, arg) @@ -3013,10 +3026,10 @@ func (q *querier) UpdateUserHashedPassword(ctx context.Context, arg database.Upd return err } - err = q.authorizeContext(ctx, policy.ActionUpdate, user.UserDataRBACObject()) + err = q.authorizeContext(ctx, policy.ActionUpdatePersonal, user) if err != nil { // Admins can update passwords for other users. - err = q.authorizeContext(ctx, policy.ActionUpdate, user.RBACObject()) + err = q.authorizeContext(ctx, policy.ActionUpdate, user) if err != nil { return err } diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 9e7777283967d..13acaaaa77487 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -100,7 +100,7 @@ func (s APIKeyScope) ToRBAC() rbac.ScopeName { } func (k APIKey) RBACObject() rbac.Object { - return rbac.ResourceAPIKey.WithIDString(k.ID). + return rbac.ResourceApiKey.WithIDString(k.ID). WithOwner(k.UserID.String()) } @@ -154,31 +154,12 @@ func (w GetWorkspaceByAgentIDRow) RBACObject() rbac.Object { } func (w Workspace) RBACObject() rbac.Object { - return rbac.ResourceWorkspace.WithID(w.ID). - InOrg(w.OrganizationID). - WithOwner(w.OwnerID.String()) -} - -func (w Workspace) ExecutionRBAC() rbac.Object { // If a workspace is locked it cannot be accessed. if w.DormantAt.Valid { return w.DormantRBAC() } - return rbac.ResourceWorkspaceExecution. - WithID(w.ID). - InOrg(w.OrganizationID). - WithOwner(w.OwnerID.String()) -} - -func (w Workspace) ApplicationConnectRBAC() rbac.Object { - // If a workspace is locked it cannot be accessed. - if w.DormantAt.Valid { - return w.DormantRBAC() - } - - return rbac.ResourceWorkspaceApplicationConnect. - WithID(w.ID). + return rbac.ResourceWorkspace.WithID(w.ID). InOrg(w.OrganizationID). WithOwner(w.OwnerID.String()) } @@ -291,15 +272,15 @@ func (l License) RBACObject() rbac.Object { } func (c OAuth2ProviderAppCode) RBACObject() rbac.Object { - return rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(c.UserID.String()) + return rbac.ResourceOauth2AppCodeToken.WithOwner(c.UserID.String()) } func (OAuth2ProviderAppSecret) RBACObject() rbac.Object { - return rbac.ResourceOAuth2ProviderAppSecret + return rbac.ResourceOauth2AppSecret } func (OAuth2ProviderApp) RBACObject() rbac.Object { - return rbac.ResourceOAuth2ProviderApp + return rbac.ResourceOauth2App } func (a GetOAuth2ProviderAppsByUserIDRow) RBACObject() rbac.Object { diff --git a/coderd/rbac/authz_internal_test.go b/coderd/rbac/authz_internal_test.go index cba69952ea481..b727d44a4f5ba 100644 --- a/coderd/rbac/authz_internal_test.go +++ b/coderd/rbac/authz_internal_test.go @@ -303,16 +303,16 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "UserACLList", user, []authTestCase{ { resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{ - user.ID: AllActions(), + user.ID: ResourceWorkspace.AvailableActions(), }), - actions: AllActions(), + actions: ResourceWorkspace.AvailableActions(), allow: true, }, { resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{ user.ID: {WildcardSymbol}, }), - actions: AllActions(), + actions: ResourceWorkspace.AvailableActions(), allow: true, }, { @@ -335,16 +335,16 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "GroupACLList", user, []authTestCase{ { resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{ - allUsersGroup: AllActions(), + allUsersGroup: ResourceWorkspace.AvailableActions(), }), - actions: AllActions(), + actions: ResourceWorkspace.AvailableActions(), allow: true, }, { resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{ allUsersGroup: {WildcardSymbol}, }), - actions: AllActions(), + actions: ResourceWorkspace.AvailableActions(), allow: true, }, { @@ -366,27 +366,27 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "Member", user, []authTestCase{ // Org + me - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true}, - {resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.All(), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + me - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + other user - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + other us - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, }) user = Subject{ @@ -407,27 +407,27 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "DeletedMember", user, []authTestCase{ // Org + me - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: false}, - {resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.All(), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + me - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + other user - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + other use - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, }) user = Subject{ @@ -441,27 +441,27 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "OrgAdmin", user, []authTestCase{ // Org + me - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true}, - {resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.All(), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + me - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + other user - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + other use - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, }) user = Subject{ @@ -475,27 +475,27 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "SiteAdmin", user, []authTestCase{ // Org + me - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true}, - {resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.All(), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: true}, // Other org + me - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: true}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: true}, // Other org + other user - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true}, // Other org + other use - {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: true}, - {resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: true}, + {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true}, }) user = Subject{ @@ -510,60 +510,60 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "ApplicationToken", user, // Create (connect) Actions cases(func(c authTestCase) authTestCase { - c.actions = []policy.Action{policy.ActionCreate} + c.actions = []policy.Action{policy.ActionApplicationConnect} return c }, []authTestCase{ // Org + me - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), allow: true}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg), allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg), allow: false}, - {resource: ResourceWorkspaceApplicationConnect.WithOwner(user.ID), allow: true}, + {resource: ResourceWorkspace.WithOwner(user.ID), allow: true}, - {resource: ResourceWorkspaceApplicationConnect.All(), allow: false}, + {resource: ResourceWorkspace.All(), allow: false}, // Other org + me - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner(user.ID), allow: false}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), allow: false}, // Other org + other user - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: false}, - {resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me"), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), allow: false}, // Other org + other use - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner("not-me"), allow: false}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false}, + {resource: ResourceWorkspace.InOrg(unuseID), allow: false}, - {resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me"), allow: false}, + {resource: ResourceWorkspace.WithOwner("not-me"), allow: false}, }), - // Not create actions + // No ActionApplicationConnect action cases(func(c authTestCase) authTestCase { c.actions = []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} c.allow = false return c }, []authTestCase{ // Org + me - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID)}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg)}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID)}, + {resource: ResourceWorkspace.InOrg(defOrg)}, - {resource: ResourceWorkspaceApplicationConnect.WithOwner(user.ID)}, + {resource: ResourceWorkspace.WithOwner(user.ID)}, - {resource: ResourceWorkspaceApplicationConnect.All()}, + {resource: ResourceWorkspace.All()}, // Other org + me - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner(user.ID)}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID)}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID)}, + {resource: ResourceWorkspace.InOrg(unuseID)}, // Other org + other user - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me")}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, - {resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me")}, + {resource: ResourceWorkspace.WithOwner("not-me")}, // Other org + other use - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner("not-me")}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID)}, + {resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")}, + {resource: ResourceWorkspace.InOrg(unuseID)}, - {resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me")}, + {resource: ResourceWorkspace.WithOwner("not-me")}, }), // Other Objects cases(func(c authTestCase) authTestCase { @@ -723,7 +723,7 @@ func TestAuthorizeLevels(t *testing.T) { testAuthorize(t, "AdminAlwaysAllow", user, cases(func(c authTestCase) authTestCase { - c.actions = AllActions() + c.actions = ResourceWorkspace.AvailableActions() c.allow = true return c }, []authTestCase{ @@ -782,7 +782,7 @@ func TestAuthorizeLevels(t *testing.T) { testAuthorize(t, "OrgAllowAll", user, cases(func(c authTestCase) authTestCase { - c.actions = AllActions() + c.actions = ResourceWorkspace.AvailableActions() return c }, []authTestCase{ // Org + me @@ -840,9 +840,9 @@ func TestAuthorizeScope(t *testing.T) { }), // Allowed by scope: []authTestCase{ - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: true}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}, allow: true}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true}, + {resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true}, }, ) @@ -875,9 +875,9 @@ func TestAuthorizeScope(t *testing.T) { }), // Allowed by scope: []authTestCase{ - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}, allow: true}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false}, - {resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: false}, + {resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: false}, }, ) diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 1f4c3545ca618..2022deee495b3 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -1,6 +1,8 @@ package rbac import ( + "fmt" + "github.com/google/uuid" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -32,6 +34,19 @@ type Object struct { ACLGroupList map[string][]policy.Action ` json:"acl_group_list"` } +// ValidAction checks if the action is valid for the given object type. +func (z Object) ValidAction(action policy.Action) error { + perms, ok := policy.RBACPermissions[z.Type] + if !ok { + return fmt.Errorf("invalid type %q", z.Type) + } + if _, ok := perms.Actions[action]; !ok { + return fmt.Errorf("invalid action %q", action) + } + + return nil +} + // AvailableActions returns all available actions for a given object. // Wildcard is omitted. func (z Object) AvailableActions() []policy.Action { diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 94abe424419a9..28d79a261e289 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -1,8 +1,6 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac -import "github.com/coder/coder/v2/coderd/rbac/policy" - // Objecter returns the RBAC object for itself. type Objecter interface { RBACObject() Object @@ -11,277 +9,227 @@ type Objecter interface { var ( // ResourceWildcard // Valid Actions - ResourceWildcard = WildcardObject{ - Object: Object{ - Type: "*", - }, + ResourceWildcard = Object{ + Type: "*", } // ResourceApiKey // Valid Actions - // - "create" needs [owner] :: create an api key - // - "delete" needs [owner] :: delete an api key - // - "read" needs [owner] :: read api key details (secrets are not stored) - ResourceApiKey = ApiKeyObject{ - Object: Object{ - Type: "api_key", - }, + // - "ActionCreate" needs [owner] :: create an api key + // - "ActionDelete" needs [owner] :: delete an api key + // - "ActionRead" needs [owner] :: read api key details (secrets are not stored) + ResourceApiKey = Object{ + Type: "api_key", } // ResourceAssignOrgRole // Valid Actions - // - "assign" needs [] :: ability to assign org scoped roles - // - "delete" needs [] :: ability to delete org scoped roles - ResourceAssignOrgRole = AssignOrgRoleObject{ - Object: Object{ - Type: "assign_org_role", - }, + // - "ActionAssign" needs [] :: ability to assign org scoped roles + // - "ActionDelete" needs [] :: ability to delete org scoped roles + ResourceAssignOrgRole = Object{ + Type: "assign_org_role", } // ResourceAssignRole // Valid Actions - // - "assign" needs [] :: ability to assign roles - // - "delete" needs [] :: ability to delete roles - // - "read" needs [] :: view what roles are assignable - ResourceAssignRole = AssignRoleObject{ - Object: Object{ - Type: "assign_role", - }, + // - "ActionAssign" needs [] :: ability to assign roles + // - "ActionDelete" needs [] :: ability to delete roles + // - "ActionRead" needs [] :: view what roles are assignable + ResourceAssignRole = Object{ + Type: "assign_role", } // ResourceAuditLog // Valid Actions - // - "read" needs [] :: read audit logs - ResourceAuditLog = AuditLogObject{ - Object: Object{ - Type: "audit_log", - }, + // - "ActionRead" needs [] :: read audit logs + ResourceAuditLog = Object{ + Type: "audit_log", } // ResourceDebugInfo // Valid Actions - // - "use" needs [] :: access to debug routes - ResourceDebugInfo = DebugInfoObject{ - Object: Object{ - Type: "debug_info", - }, + // - "ActionUse" needs [] :: access to debug routes + ResourceDebugInfo = Object{ + Type: "debug_info", } // ResourceDeploymentConfig // Valid Actions - // - "read" needs [] :: read deployment config - ResourceDeploymentConfig = DeploymentConfigObject{ - Object: Object{ - Type: "deployment_config", - }, + // - "ActionRead" needs [] :: read deployment config + ResourceDeploymentConfig = Object{ + Type: "deployment_config", } // ResourceDeploymentStats // Valid Actions - // - "read" needs [] :: read deployment stats - ResourceDeploymentStats = DeploymentStatsObject{ - Object: Object{ - Type: "deployment_stats", - }, + // - "ActionRead" needs [] :: read deployment stats + ResourceDeploymentStats = Object{ + Type: "deployment_stats", } // ResourceFile // Valid Actions - // - "create" needs [] :: create a file - // - "read" needs [] :: read files - ResourceFile = FileObject{ - Object: Object{ - Type: "file", - }, + // - "ActionCreate" needs [] :: create a file + // - "ActionRead" needs [] :: read files + ResourceFile = Object{ + Type: "file", } // ResourceGroup // Valid Actions - // - "create" needs [org] :: create a group - // - "delete" needs [org] :: delete a group - // - "read" needs [org] :: read groups - // - "update" needs [org] :: update a group - ResourceGroup = GroupObject{ - Object: Object{ - Type: "group", - }, + // - "ActionCreate" needs [org] :: create a group + // - "ActionDelete" needs [org] :: delete a group + // - "ActionRead" needs [org] :: read groups + // - "ActionUpdate" needs [org] :: update a group + ResourceGroup = Object{ + Type: "group", } // ResourceLicense // Valid Actions - // - "create" needs [] :: create a license - // - "delete" needs [] :: delete license - // - "read" needs [] :: read licenses - ResourceLicense = LicenseObject{ - Object: Object{ - Type: "license", - }, + // - "ActionCreate" needs [] :: create a license + // - "ActionDelete" needs [] :: delete license + // - "ActionRead" needs [] :: read licenses + ResourceLicense = Object{ + Type: "license", } // ResourceOauth2App // Valid Actions - // - "create" needs [] :: make an OAuth2 app. - // - "delete" needs [] :: delete an OAuth2 app - // - "read" needs [] :: read OAuth2 apps - // - "update" needs [] :: update the properties of the OAuth2 app. - ResourceOauth2App = Oauth2AppObject{ - Object: Object{ - Type: "oauth2_app", - }, + // - "ActionCreate" needs [] :: make an OAuth2 app. + // - "ActionDelete" needs [] :: delete an OAuth2 app + // - "ActionRead" needs [] :: read OAuth2 apps + // - "ActionUpdate" needs [] :: update the properties of the OAuth2 app. + ResourceOauth2App = Object{ + Type: "oauth2_app", } // ResourceOauth2AppCodeToken // Valid Actions - // - "create" needs [] :: - // - "delete" needs [] :: - // - "read" needs [] :: - ResourceOauth2AppCodeToken = Oauth2AppCodeTokenObject{ - Object: Object{ - Type: "oauth2_app_code_token", - }, + // - "ActionCreate" needs [] :: + // - "ActionDelete" needs [] :: + // - "ActionRead" needs [] :: + ResourceOauth2AppCodeToken = Object{ + Type: "oauth2_app_code_token", } // ResourceOauth2AppSecret // Valid Actions - // - "create" needs [] :: - // - "delete" needs [] :: - // - "read" needs [] :: - // - "update" needs [] :: - ResourceOauth2AppSecret = Oauth2AppSecretObject{ - Object: Object{ - Type: "oauth2_app_secret", - }, + // - "ActionCreate" needs [] :: + // - "ActionDelete" needs [] :: + // - "ActionRead" needs [] :: + // - "ActionUpdate" needs [] :: + ResourceOauth2AppSecret = Object{ + Type: "oauth2_app_secret", } // ResourceOrganization // Valid Actions - // - "create" needs [] :: create an organization - // - "delete" needs [] :: delete a organization - // - "read" needs [] :: read organizations - ResourceOrganization = OrganizationObject{ - Object: Object{ - Type: "organization", - }, + // - "ActionCreate" needs [] :: create an organization + // - "ActionDelete" needs [] :: delete a organization + // - "ActionRead" needs [] :: read organizations + ResourceOrganization = Object{ + Type: "organization", } // ResourceOrganizationMember // Valid Actions - // - "create" needs [org] :: create an organization member - // - "delete" needs [org] :: delete member - // - "read" needs [org] :: read member - // - "update" needs [org] :: update a organization member - ResourceOrganizationMember = OrganizationMemberObject{ - Object: Object{ - Type: "organization_member", - }, + // - "ActionCreate" needs [org] :: create an organization member + // - "ActionDelete" needs [org] :: delete member + // - "ActionRead" needs [org] :: read member + // - "ActionUpdate" needs [org] :: update a organization member + ResourceOrganizationMember = Object{ + Type: "organization_member", } // ResourceProvisionerDaemon // Valid Actions - // - "create" needs [org] :: create a provisioner daemon - // - "delete" needs [org] :: delete a provisioner daemon - // - "read" needs [org] :: read provisioner daemon - // - "update" needs [org] :: update a provisioner daemon - ResourceProvisionerDaemon = ProvisionerDaemonObject{ - Object: Object{ - Type: "provisioner_daemon", - }, + // - "ActionCreate" needs [org] :: create a provisioner daemon + // - "ActionDelete" needs [org] :: delete a provisioner daemon + // - "ActionRead" needs [org] :: read provisioner daemon + // - "ActionUpdate" needs [org] :: update a provisioner daemon + ResourceProvisionerDaemon = Object{ + Type: "provisioner_daemon", } // ResourceReplicas // Valid Actions - // - "read" needs [] :: read replicas - ResourceReplicas = ReplicasObject{ - Object: Object{ - Type: "replicas", - }, + // - "ActionRead" needs [] :: read replicas + ResourceReplicas = Object{ + Type: "replicas", } // ResourceSystem // Valid Actions - // - "create" needs [] :: create system resources - // - "delete" needs [] :: delete system resources - // - "read" needs [] :: view system resources - // - "update" needs [] :: update system resources - ResourceSystem = SystemObject{ - Object: Object{ - Type: "system", - }, + // - "ActionCreate" needs [] :: create system resources + // - "ActionDelete" needs [] :: delete system resources + // - "ActionRead" needs [] :: view system resources + // - "ActionUpdate" needs [] :: update system resources + ResourceSystem = Object{ + Type: "system", } // ResourceTailnetCoordinator // Valid Actions - // - "create" needs [] :: - // - "delete" needs [] :: - // - "read" needs [] :: - // - "update" needs [] :: - ResourceTailnetCoordinator = TailnetCoordinatorObject{ - Object: Object{ - Type: "tailnet_coordinator", - }, + // - "ActionCreate" needs [] :: + // - "ActionDelete" needs [] :: + // - "ActionRead" needs [] :: + // - "ActionUpdate" needs [] :: + ResourceTailnetCoordinator = Object{ + Type: "tailnet_coordinator", } // ResourceTemplate // Valid Actions - // - "create" needs [org] :: create a template - // - "delete" needs [org,acl] :: delete a template - // - "read" needs [org,acl] :: read template - // - "update" needs [org,acl] :: update a template - // - "view_insights" needs [org,acl] :: view insights - ResourceTemplate = TemplateObject{ - Object: Object{ - Type: "template", - }, + // - "ActionCreate" needs [org] :: create a template + // - "ActionDelete" needs [org,acl] :: delete a template + // - "ActionRead" needs [org,acl] :: read template + // - "ActionUpdate" needs [org,acl] :: update a template + // - "ActionViewInsights" needs [org,acl] :: view insights + ResourceTemplate = Object{ + Type: "template", } // ResourceUser // Valid Actions - // - "create" needs [] :: create a new user - // - "delete" needs [] :: delete an existing user - // - "read" needs [] :: read user data - // - "read_personal" needs [owner] :: read personal user data like password - // - "update" needs [] :: update an existing user - // - "update_personal" needs [owner] :: update personal data - ResourceUser = UserObject{ - Object: Object{ - Type: "user", - }, + // - "ActionCreate" needs [] :: create a new user + // - "ActionDelete" needs [] :: delete an existing user + // - "ActionRead" needs [] :: read user data + // - "ActionReadPersonal" needs [owner] :: read personal user data like password + // - "ActionUpdate" needs [] :: update an existing user + // - "ActionUpdatePersonal" needs [owner] :: update personal data + ResourceUser = Object{ + Type: "user", } // ResourceWorkspace // Valid Actions - // - "application_connect" needs [owner,org,acl] :: connect to workspace apps via browser - // - "build" needs [owner,org,acl] :: allows starting, stopping, and updating a workspace - // - "build_parameters" needs [owner,org,acl] :: view workspace build parameters - // - "create" needs [owner,org] :: create a new workspace - // - "delete" needs [owner,org,acl] :: delete workspace - // - "read" needs [owner,org,acl] :: read workspace data to view on the UI - // - "ssh" needs [owner,org,acl] :: ssh into a given workspace - // - "update" needs [owner,org,acl] :: edit workspace settings (scheduling, permissions, parameters) - ResourceWorkspace = WorkspaceObject{ - Object: Object{ - Type: "workspace", - }, + // - "ActionApplicationConnect" needs [owner,org,acl] :: connect to workspace apps via browser + // - "ActionWorkspaceBuild" needs [owner,org,acl] :: allows starting, stopping, and updating a workspace + // - "ActionViewWorkspaceBuildParams" needs [owner,org,acl] :: view workspace build parameters + // - "ActionCreate" needs [owner,org] :: create a new workspace + // - "ActionDelete" needs [owner,org,acl] :: delete workspace + // - "ActionRead" needs [owner,org,acl] :: read workspace data to view on the UI + // - "ActionSSH" needs [owner,org,acl] :: ssh into a given workspace + // - "ActionUpdate" needs [owner,org,acl] :: edit workspace settings (scheduling, permissions, parameters) + ResourceWorkspace = Object{ + Type: "workspace", } // ResourceWorkspaceDormant // Valid Actions - ResourceWorkspaceDormant = WorkspaceDormantObject{ - Object: Object{ - Type: "workspace_dormant", - }, + ResourceWorkspaceDormant = Object{ + Type: "workspace_dormant", } // ResourceWorkspaceProxy // Valid Actions - // - "create" needs [] :: create a workspace proxy - // - "delete" needs [] :: delete a workspace proxy - // - "read" needs [] :: read and use a workspace proxy - // - "update" needs [] :: update a workspace proxy - ResourceWorkspaceProxy = WorkspaceProxyObject{ - Object: Object{ - Type: "workspace_proxy", - }, + // - "ActionCreate" needs [] :: create a workspace proxy + // - "ActionDelete" needs [] :: delete a workspace proxy + // - "ActionRead" needs [] :: read and use a workspace proxy + // - "ActionUpdate" needs [] :: update a workspace proxy + ResourceWorkspaceProxy = Object{ + Type: "workspace_proxy", } ) @@ -314,155 +262,3 @@ func AllResources() []Objecter { ResourceWorkspaceProxy, } } - -type WildcardObject struct{ Object } - -type ApiKeyObject struct{ Object } - -func (ApiKeyObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (ApiKeyObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (ApiKeyObject) ActionRead() policy.Action { return policy.ActionRead } - -type AssignOrgRoleObject struct{ Object } - -func (AssignOrgRoleObject) ActionAssign() policy.Action { return policy.ActionAssign } -func (AssignOrgRoleObject) ActionDelete() policy.Action { return policy.ActionDelete } - -type AssignRoleObject struct{ Object } - -func (AssignRoleObject) ActionAssign() policy.Action { return policy.ActionAssign } -func (AssignRoleObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (AssignRoleObject) ActionRead() policy.Action { return policy.ActionRead } - -type AuditLogObject struct{ Object } - -func (AuditLogObject) ActionRead() policy.Action { return policy.ActionRead } - -type DebugInfoObject struct{ Object } - -func (DebugInfoObject) ActionUse() policy.Action { return policy.ActionUse } - -type DeploymentConfigObject struct{ Object } - -func (DeploymentConfigObject) ActionRead() policy.Action { return policy.ActionRead } - -type DeploymentStatsObject struct{ Object } - -func (DeploymentStatsObject) ActionRead() policy.Action { return policy.ActionRead } - -type FileObject struct{ Object } - -func (FileObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (FileObject) ActionRead() policy.Action { return policy.ActionRead } - -type GroupObject struct{ Object } - -func (GroupObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (GroupObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (GroupObject) ActionRead() policy.Action { return policy.ActionRead } -func (GroupObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type LicenseObject struct{ Object } - -func (LicenseObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (LicenseObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (LicenseObject) ActionRead() policy.Action { return policy.ActionRead } - -type Oauth2AppObject struct{ Object } - -func (Oauth2AppObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (Oauth2AppObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (Oauth2AppObject) ActionRead() policy.Action { return policy.ActionRead } -func (Oauth2AppObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type Oauth2AppCodeTokenObject struct{ Object } - -func (Oauth2AppCodeTokenObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (Oauth2AppCodeTokenObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (Oauth2AppCodeTokenObject) ActionRead() policy.Action { return policy.ActionRead } - -type Oauth2AppSecretObject struct{ Object } - -func (Oauth2AppSecretObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (Oauth2AppSecretObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (Oauth2AppSecretObject) ActionRead() policy.Action { return policy.ActionRead } -func (Oauth2AppSecretObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type OrganizationObject struct{ Object } - -func (OrganizationObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (OrganizationObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (OrganizationObject) ActionRead() policy.Action { return policy.ActionRead } - -type OrganizationMemberObject struct{ Object } - -func (OrganizationMemberObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (OrganizationMemberObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (OrganizationMemberObject) ActionRead() policy.Action { return policy.ActionRead } -func (OrganizationMemberObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type ProvisionerDaemonObject struct{ Object } - -func (ProvisionerDaemonObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (ProvisionerDaemonObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (ProvisionerDaemonObject) ActionRead() policy.Action { return policy.ActionRead } -func (ProvisionerDaemonObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type ReplicasObject struct{ Object } - -func (ReplicasObject) ActionRead() policy.Action { return policy.ActionRead } - -type SystemObject struct{ Object } - -func (SystemObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (SystemObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (SystemObject) ActionRead() policy.Action { return policy.ActionRead } -func (SystemObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type TailnetCoordinatorObject struct{ Object } - -func (TailnetCoordinatorObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (TailnetCoordinatorObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (TailnetCoordinatorObject) ActionRead() policy.Action { return policy.ActionRead } -func (TailnetCoordinatorObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type TemplateObject struct{ Object } - -func (TemplateObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (TemplateObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (TemplateObject) ActionRead() policy.Action { return policy.ActionRead } -func (TemplateObject) ActionUpdate() policy.Action { return policy.ActionUpdate } -func (TemplateObject) ActionViewInsights() policy.Action { return policy.ActionViewInsights } - -type UserObject struct{ Object } - -func (UserObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (UserObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (UserObject) ActionRead() policy.Action { return policy.ActionRead } -func (UserObject) ActionReadPersonal() policy.Action { return policy.ActionReadPersonal } -func (UserObject) ActionUpdate() policy.Action { return policy.ActionUpdate } -func (UserObject) ActionUpdatePersonal() policy.Action { return policy.ActionUpdatePersonal } - -type WorkspaceObject struct{ Object } - -func (WorkspaceObject) ActionApplicationConnect() policy.Action { - return policy.ActionApplicationConnect -} -func (WorkspaceObject) ActionWorkspaceBuild() policy.Action { return policy.ActionWorkspaceBuild } -func (WorkspaceObject) ActionViewWorkspaceBuildParams() policy.Action { - return policy.ActionViewWorkspaceBuildParams -} -func (WorkspaceObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (WorkspaceObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (WorkspaceObject) ActionRead() policy.Action { return policy.ActionRead } -func (WorkspaceObject) ActionSSH() policy.Action { return policy.ActionSSH } -func (WorkspaceObject) ActionUpdate() policy.Action { return policy.ActionUpdate } - -type WorkspaceDormantObject struct{ Object } - -type WorkspaceProxyObject struct{ Object } - -func (WorkspaceProxyObject) ActionCreate() policy.Action { return policy.ActionCreate } -func (WorkspaceProxyObject) ActionDelete() policy.Action { return policy.ActionDelete } -func (WorkspaceProxyObject) ActionRead() policy.Action { return policy.ActionRead } -func (WorkspaceProxyObject) ActionUpdate() policy.Action { return policy.ActionUpdate } diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 5ebd2b7f66df8..f0e660a698df5 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -129,7 +129,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // Remove ssh and application connect from the owner role. This // prevents owners from have exec access to all workspaces. ownerWorkspaceActions = slice.Omit(ownerWorkspaceActions, - ResourceWorkspace.ActionApplicationConnect(), ResourceWorkspace.ActionSSH()) + policy.ActionApplicationConnect, policy.ActionSSH) } // Static roles that never change should be allocated in a closure. @@ -155,21 +155,21 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Name: member, DisplayName: "Member", Site: Permissions(map[string][]policy.Action{ - ResourceRoleAssignment.Type: {ActionRead}, + ResourceAssignRole.Type: {policy.ActionRead}, // All users can see the provisioner daemons. - ResourceProvisionerDaemon.Type: {ResourceProvisionerDaemon.ActionRead()}, + ResourceProvisionerDaemon.Type: {policy.ActionRead}, // All users can see OAuth2 provider applications. - ResourceOAuth2ProviderApp.Type: {ActionRead}, + ResourceOauth2App.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember), Permissions(map[string][]policy.Action{ // Users cannot do create/update/delete on themselves, but they // can read their own details. - ResourceUser.Type: {policy.ActionRead}, - ResourceUserWorkspaceBuildParameters.Type: {ActionRead}, + ResourceUser.Type: {policy.ActionRead}, + ResourceWorkspace.Type: {policy.ActionViewWorkspaceBuildParams}, // Users can create provisioner daemons scoped to themselves. - ResourceProvisionerDaemon.Type: {ResourceProvisionerDaemon.ActionRead(), ActionCreate, ActionRead, ActionUpdate}, + ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, })..., ), }.withCachedRegoValue() @@ -180,7 +180,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Site: Permissions(map[string][]policy.Action{ // Should be able to read all template details, even in orgs they // are not in. - ResourceTemplate.Type: {policy.ActionRead, ResourceTemplate.ActionViewInsights()}, + ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, ResourceAuditLog.Type: {policy.ActionRead}, ResourceUser.Type: {policy.ActionRead}, ResourceGroup.Type: {policy.ActionRead}, @@ -188,7 +188,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceDeploymentStats.Type: {policy.ActionRead}, ResourceDeploymentConfig.Type: {policy.ActionRead}, // Org roles are not really used yet, so grant the perm at the site level. - ResourceOrganizationMember.Type: {ActionRead}, + ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -198,20 +198,18 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Name: templateAdmin, DisplayName: "Template Admin", Site: Permissions(map[string][]policy.Action{ - ResourceTemplate.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceTemplate.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights}, // CRUD all files, even those they did not upload. - ResourceFile.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, - ResourceWorkspace.Type: {ActionRead}, + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + ResourceWorkspace.Type: {policy.ActionRead}, // CRUD to provisioner daemons for now. - ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, // Needs to read all organizations since - ResourceOrganization.Type: {ActionRead}, - ResourceUser.Type: {ActionRead}, - ResourceGroup.Type: {ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, // Org roles are not really used yet, so grant the perm at the site level. - ResourceOrganizationMember.Type: {ActionRead}, - // Template admins can read all template insights data - ResourceTemplateInsights.Type: {ActionRead}, + ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -221,13 +219,15 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Name: userAdmin, DisplayName: "User Admin", Site: Permissions(map[string][]policy.Action{ - ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, - ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, - ResourceUserData.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, - ResourceUserWorkspaceBuildParameters.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead}, + ResourceUser.Type: { + policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, + policy.ActionUpdatePersonal, policy.ActionReadPersonal, + }, + ResourceWorkspace.Type: {policy.ActionViewWorkspaceBuildParams}, // Full perms to manage org members - ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, - ResourceGroup.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -268,7 +268,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Site: []Permission{}, Org: map[string][]Permission{ // Org admins should not have workspace exec perms. - organizationID: allPermsExcept(ResourceWorkspaceExecution, ResourceWorkspaceDormant), + organizationID: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant), Permissions(map[string][]policy.Action{ + ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), + })...), }, User: []Permission{}, } @@ -286,19 +288,19 @@ func ReloadBuiltinRoles(opts *RoleOptions) { { // All org members can read the organization ResourceType: ResourceOrganization.Type, - Action: ActionRead, + Action: policy.ActionRead, }, { // Can read available roles. - ResourceType: ResourceOrgRoleAssignment.Type, - Action: ActionRead, + ResourceType: ResourceAssignOrgRole.Type, + Action: policy.ActionRead, }, }, }, User: []Permission{ { ResourceType: ResourceOrganizationMember.Type, - Action: ActionRead, + Action: policy.ActionRead, }, }, } diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index d79d191af9ce5..9faae72f22ef7 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1030,7 +1030,7 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R // This route accepts user API key auth and workspace proxy auth. The moon actor has // full permissions so should be able to pass this authz check. workspace := httpmw.WorkspaceParam(r) - if !api.Authorize(r, policy.ActionCreate, workspace.ExecutionRBAC()) { + if !api.Authorize(r, policy.ActionSSH, workspace) { httpapi.ResourceNotFound(rw) return } diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go index 144de2f2573f9..1b369cf6d6ef4 100644 --- a/coderd/workspaceapps/db.go +++ b/coderd/workspaceapps/db.go @@ -282,16 +282,16 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *rbac.Subj // Figure out which RBAC resource to check. For terminals we use execution // instead of application connect. var ( - rbacAction policy.Action = policy.ActionCreate - rbacResource rbac.Object = dbReq.Workspace.ApplicationConnectRBAC() + rbacAction policy.Action = policy.ActionApplicationConnect + rbacResource rbac.Object = dbReq.Workspace.RBACObject() // rbacResourceOwned is for the level "authenticated". We still need to // make sure the API key has permissions to connect to the actor's own // workspace. Scopes would prevent this. - rbacResourceOwned rbac.Object = rbac.ResourceWorkspaceApplicationConnect.WithOwner(roles.ID) + rbacResourceOwned rbac.Object = rbac.ResourceWorkspace.WithOwner(roles.ID) ) if dbReq.AccessMethod == AccessMethodTerminal { - rbacResource = dbReq.Workspace.ExecutionRBAC() - rbacResourceOwned = rbac.ResourceWorkspaceExecution.WithOwner(roles.ID) + rbacAction = policy.ActionSSH + rbacResourceOwned = rbac.ResourceWorkspace.WithOwner(roles.ID) } // Do a standard RBAC check. This accounts for share level "owner" and any diff --git a/enterprise/coderd/templates.go b/enterprise/coderd/templates.go index feddcce4d8372..cf67179984b51 100644 --- a/enterprise/coderd/templates.go +++ b/enterprise/coderd/templates.go @@ -310,7 +310,7 @@ func convertToTemplateRole(actions []policy.Action) codersdk.TemplateRole { switch { case len(actions) == 1 && actions[0] == policy.ActionRead: return codersdk.TemplateRoleUse - case len(actions) == 1 && actions[0] == rbac.WildcardSymbol: + case len(actions) == 1 && actions[0] == policy.WildcardSymbol: return codersdk.TemplateRoleAdmin } diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/object.gotmpl index 8feb9a4a3cb21..af766cb57d6fd 100644 --- a/scripts/rbacgen/object.gotmpl +++ b/scripts/rbacgen/object.gotmpl @@ -1,8 +1,6 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac -import "github.com/coder/coder/v2/coderd/rbac/policy" - // Objecter returns the RBAC object for itself. type Objecter interface { RBACObject() Object @@ -14,12 +12,10 @@ var ( // Resource{{ $Name }} // Valid Actions {{- range $action, $value := .Actions }} - // - "{{ $action }}" needs [{{ $value.Requires }}] :: {{ $value.Description }} + // - "{{ actionEnum $action }}" needs [{{ $value.Requires }}] :: {{ $value.Description }} {{- end }} - Resource{{ $Name }} = {{ $Name }}Object { - Object: Object{ - Type: "{{ $element.Type }}", - }, + Resource{{ $Name }} = Object { + Type: "{{ $element.Type }}", } {{ end -}} ) @@ -31,12 +27,3 @@ func AllResources() []Objecter { {{- end }} } } - -{{- range $element := . }} - {{- $Name := pascalCaseName $element.FunctionName }} - type {{ $Name }}Object struct{ Object } - {{- range $action, $value := .Actions -}} - {{ $ActionEnum := actionEnum $action }} - func ({{ $Name }}Object ) {{ $ActionEnum }}() policy.Action { return policy.{{ $ActionEnum }} } - {{- end }} -{{ end -}} From 1e1778dba67549388203b98a64af632b80a36b7a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 13:11:00 -0500 Subject: [PATCH 10/24] More dbauthz --- coderd/database/dbauthz/dbauthz.go | 153 +++++++++-------------------- 1 file changed, 48 insertions(+), 105 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 10eaf2a1c4ba4..0e9d489f68703 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -837,14 +837,14 @@ func (q *querier) DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.U func (q *querier) DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, - rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(arg.UserID.String())); err != nil { + rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil { return err } return q.db.DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx, arg) } func (q *querier) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOAuth2ProviderAppSecret); err != nil { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOauth2AppSecret); err != nil { return err } return q.db.DeleteOAuth2ProviderAppSecretByID(ctx, id) @@ -852,7 +852,7 @@ func (q *querier) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid func (q *querier) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg database.DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, - rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(arg.UserID.String())); err != nil { + rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil { return err } return q.db.DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx, arg) @@ -1241,7 +1241,7 @@ func (q *querier) GetNotificationBanners(ctx context.Context) (string, error) { } func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOAuth2ProviderApp); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.GetOAuth2ProviderAppByID(ctx, id) @@ -1256,7 +1256,7 @@ func (q *querier) GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPr } func (q *querier) GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderAppSecret, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOAuth2ProviderAppSecret); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppSecret); err != nil { return database.OAuth2ProviderAppSecret{}, err } return q.db.GetOAuth2ProviderAppSecretByID(ctx, id) @@ -1267,7 +1267,7 @@ func (q *querier) GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secret } func (q *querier) GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]database.OAuth2ProviderAppSecret, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOAuth2ProviderAppSecret); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppSecret); err != nil { return []database.OAuth2ProviderAppSecret{}, err } return q.db.GetOAuth2ProviderAppSecretsByAppID(ctx, appID) @@ -1283,14 +1283,14 @@ func (q *querier) GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPre if err != nil { return database.OAuth2ProviderAppToken{}, err } - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(key.UserID.String())); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppCodeToken.WithOwner(key.UserID.String())); err != nil { return database.OAuth2ProviderAppToken{}, err } return token, nil } func (q *querier) GetOAuth2ProviderApps(ctx context.Context) ([]database.OAuth2ProviderApp, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOAuth2ProviderApp); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil { return []database.OAuth2ProviderApp{}, err } return q.db.GetOAuth2ProviderApps(ctx) @@ -1299,7 +1299,7 @@ func (q *querier) GetOAuth2ProviderApps(ctx context.Context) ([]database.OAuth2P func (q *querier) GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]database.GetOAuth2ProviderAppsByUserIDRow, error) { // This authz check is to make sure the caller can read all their own tokens. if err := q.authorizeContext(ctx, policy.ActionRead, - rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(userID.String())); err != nil { + rbac.ResourceOauth2AppCodeToken.WithOwner(userID.String())); err != nil { return []database.GetOAuth2ProviderAppsByUserIDRow{}, err } return q.db.GetOAuth2ProviderAppsByUserID(ctx, userID) @@ -1510,31 +1510,15 @@ func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) } func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { - // Used by TemplateAppInsights endpoint - // For auditors, check read template_insights, and fall back to update template. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { - for _, templateID := range arg.TemplateIDs { - template, err := q.db.GetTemplateByID(ctx, templateID) - if err != nil { - return nil, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { - return nil, err - } - } - if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { - return nil, err - } - } + if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { + return nil, err } return q.db.GetTemplateAppInsights(ctx, arg) } func (q *querier) GetTemplateAppInsightsByTemplate(ctx context.Context, arg database.GetTemplateAppInsightsByTemplateParams) ([]database.GetTemplateAppInsightsByTemplateRow, error) { // Only used by prometheus metrics, so we don't strictly need to check update template perms. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { return nil, err } return q.db.GetTemplateAppInsightsByTemplate(ctx, arg) @@ -1564,102 +1548,61 @@ func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateD return q.db.GetTemplateDAUs(ctx, arg) } -func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) { - // Used by TemplateInsights endpoint - // For auditors, check read template_insights, and fall back to update template. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { - for _, templateID := range arg.TemplateIDs { +func (q *querier) authorizeTemplateInsights(ctx context.Context, templateIDs []uuid.UUID) error { + // Abort early if can read all template insights, aka admins. + // TODO: If we know the org, that would allow org admins to abort early too. + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { + for _, templateID := range templateIDs { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { - return database.GetTemplateInsightsRow{}, err + return err } - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { - return database.GetTemplateInsightsRow{}, err + if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { + return err } } - if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { - return database.GetTemplateInsightsRow{}, err + if len(templateIDs) == 0 { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { + return err } } } + return nil +} + +func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) { + if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { + return database.GetTemplateInsightsRow{}, err + } return q.db.GetTemplateInsights(ctx, arg) } func (q *querier) GetTemplateInsightsByInterval(ctx context.Context, arg database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) { - // Used by TemplateInsights endpoint - // For auditors, check read template_insights, and fall back to update template. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { - for _, templateID := range arg.TemplateIDs { - template, err := q.db.GetTemplateByID(ctx, templateID) - if err != nil { - return nil, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { - return nil, err - } - } - if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { - return nil, err - } - } + if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { + return nil, err } return q.db.GetTemplateInsightsByInterval(ctx, arg) } func (q *querier) GetTemplateInsightsByTemplate(ctx context.Context, arg database.GetTemplateInsightsByTemplateParams) ([]database.GetTemplateInsightsByTemplateRow, error) { // Only used by prometheus metrics collector. No need to check update template perms. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { return nil, err } return q.db.GetTemplateInsightsByTemplate(ctx, arg) } func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) { - // Used by both insights endpoint and prometheus collector. - // For auditors, check read template_insights, and fall back to update template. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { - for _, templateID := range arg.TemplateIDs { - template, err := q.db.GetTemplateByID(ctx, templateID) - if err != nil { - return nil, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { - return nil, err - } - } - if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { - return nil, err - } - } + if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { + return nil, err } return q.db.GetTemplateParameterInsights(ctx, arg) } func (q *querier) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) { - // Used by dbrollup tests, use same safe-guard as other insights endpoints. - // For auditors, check read template_insights, and fall back to update template. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplateInsights); err != nil { - for _, templateID := range arg.TemplateIDs { - template, err := q.db.GetTemplateByID(ctx, templateID) - if err != nil { - return nil, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { - return nil, err - } - } - if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { - return nil, err - } - } + if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { + return nil, err } return q.db.GetTemplateUsageStats(ctx, arg) } @@ -2291,7 +2234,7 @@ func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now ti func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { return insert(q.log, q.auth, - rbac.ResourceAPIKey.WithOwner(arg.UserID.String()), + rbac.ResourceApiKey.WithOwner(arg.UserID.String()), q.db.InsertAPIKey)(ctx, arg) } @@ -2363,7 +2306,7 @@ func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMi } func (q *querier) InsertOAuth2ProviderApp(ctx context.Context, arg database.InsertOAuth2ProviderAppParams) (database.OAuth2ProviderApp, error) { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOAuth2ProviderApp); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.InsertOAuth2ProviderApp(ctx, arg) @@ -2371,14 +2314,14 @@ func (q *querier) InsertOAuth2ProviderApp(ctx context.Context, arg database.Inse func (q *querier) InsertOAuth2ProviderAppCode(ctx context.Context, arg database.InsertOAuth2ProviderAppCodeParams) (database.OAuth2ProviderAppCode, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, - rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(arg.UserID.String())); err != nil { + rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil { return database.OAuth2ProviderAppCode{}, err } return q.db.InsertOAuth2ProviderAppCode(ctx, arg) } func (q *querier) InsertOAuth2ProviderAppSecret(ctx context.Context, arg database.InsertOAuth2ProviderAppSecretParams) (database.OAuth2ProviderAppSecret, error) { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOAuth2ProviderAppSecret); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2AppSecret); err != nil { return database.OAuth2ProviderAppSecret{}, err } return q.db.InsertOAuth2ProviderAppSecret(ctx, arg) @@ -2389,7 +2332,7 @@ func (q *querier) InsertOAuth2ProviderAppToken(ctx context.Context, arg database if err != nil { return database.OAuth2ProviderAppToken{}, err } - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(key.UserID.String())); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2AppCodeToken.WithOwner(key.UserID.String())); err != nil { return database.OAuth2ProviderAppToken{}, err } return q.db.InsertOAuth2ProviderAppToken(ctx, arg) @@ -2779,14 +2722,14 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb } func (q *querier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOAuth2ProviderApp); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.UpdateOAuth2ProviderAppByID(ctx, arg) } func (q *querier) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppSecretByIDParams) (database.OAuth2ProviderAppSecret, error) { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOAuth2ProviderAppSecret); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOauth2AppSecret); err != nil { return database.OAuth2ProviderAppSecret{}, err } return q.db.UpdateOAuth2ProviderAppSecretByID(ctx, arg) @@ -3324,7 +3267,7 @@ func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error { } func (q *querier) UpsertApplicationName(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentValues); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertApplicationName(ctx, value) @@ -3338,7 +3281,7 @@ func (q *querier) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDef } func (q *querier) UpsertHealthSettings(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentValues); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertHealthSettings(ctx, value) @@ -3373,14 +3316,14 @@ func (q *querier) UpsertLastUpdateCheck(ctx context.Context, value string) error } func (q *querier) UpsertLogoURL(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentValues); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertLogoURL(ctx, value) } func (q *querier) UpsertNotificationBanners(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentValues); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertNotificationBanners(ctx, value) From c17006830368c9414cf6ba23888021701bcf9323 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 15:18:09 -0500 Subject: [PATCH 11/24] roles are personal user data --- coderd/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/users.go b/coderd/users.go index f24abda349c40..c8ca04e390c7f 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1022,7 +1022,7 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() user := httpmw.UserParam(r) - if !api.Authorize(r, policy.ActionRead, user) { + if !api.Authorize(r, policy.ActionReadPersonal, user) { httpapi.ResourceNotFound(rw) return } From 3cc6ae0ffe2086d65bc48534827fd0b1f4a029e9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 15:34:29 -0500 Subject: [PATCH 12/24] remove user data object, and just use a data object --- coderd/database/dbauthz/dbauthz.go | 110 ++++++++++++---------- coderd/database/dbauthz/dbauthz_test.go | 118 ++++++++++++------------ coderd/database/modelmethods.go | 42 +-------- coderd/debug.go | 2 +- coderd/deployment.go | 2 +- coderd/insights.go | 2 +- coderd/rbac/object_gen.go | 19 ++++ coderd/rbac/policy/policy.go | 8 +- coderd/rbac/roles.go | 4 +- coderd/roles.go | 4 +- coderd/wsbuilder/wsbuilder.go | 2 +- enterprise/coderd/appearance.go | 2 +- scripts/rbacgen/main.go | 7 ++ scripts/rbacgen/object.gotmpl | 10 ++ 14 files changed, 170 insertions(+), 162 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 0e9d489f68703..5a15dc9cd9dbc 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -22,7 +22,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints" "github.com/coder/coder/v2/coderd/rbac" - "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/provisionersdk" ) @@ -165,14 +164,13 @@ var ( DisplayName: "Provisioner Daemon", Site: rbac.Permissions(map[string][]policy.Action{ // TODO: Add ProvisionerJob resource type. - rbac.ResourceFile.Type: {policy.ActionRead}, - rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, - rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, - rbac.ResourceUser.Type: {policy.ActionRead}, - rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - rbac.ResourceWorkspaceBuild.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - rbac.ResourceUserData.Type: {policy.ActionRead, policy.ActionUpdate}, - rbac.ResourceAPIKey.Type: {rbac.WildcardSymbol}, + rbac.ResourceFile.Type: {policy.ActionRead}, + rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, + rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, + // Unsure why provisionerd needs update and read personal + rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, + rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceBuild}, + rbac.ResourceApiKey.Type: {rbac.WildcardSymbol}, // When org scoped provisioner credentials are implemented, // this can be reduced to read a specific org. rbac.ResourceOrganization.Type: {policy.ActionRead}, @@ -234,19 +232,15 @@ var ( DisplayName: "Coder", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceWildcard.Type: {policy.ActionRead}, - rbac.ResourceAPIKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + rbac.ResourceApiKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate}, - rbac.ResourceRoleAssignment.Type: {policy.ActionCreate, policy.ActionDelete}, + rbac.ResourceAssignRole.Type: {policy.ActionCreate, policy.ActionDelete}, rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceOrganizationMember.Type: {policy.ActionCreate}, - rbac.ResourceOrgRoleAssignment.Type: {policy.ActionCreate}, rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate}, - rbac.ResourceUser.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, - rbac.ResourceUserData.Type: {policy.ActionCreate, policy.ActionUpdate}, - rbac.ResourceWorkspace.Type: {policy.ActionUpdate}, - rbac.ResourceWorkspaceBuild.Type: {policy.ActionUpdate}, - rbac.ResourceWorkspaceExecution.Type: {policy.ActionCreate}, + rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(), + rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceBuild, policy.ActionSSH}, rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, }), Org: map[string][]rbac.Permission{}, @@ -398,13 +392,14 @@ func update[ // The database query function will **ALWAYS** hit the database, even if the // user cannot read the resource. This is because the resource details are // required to run a proper authorization check. -func fetch[ +func fetchWithAction[ ArgumentType any, ObjectType rbac.Objecter, DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, + action policy.Action, f DatabaseFunc, ) DatabaseFunc { return func(ctx context.Context, arg ArgumentType) (empty ObjectType, err error) { @@ -421,7 +416,7 @@ func fetch[ } // Authorize the action - err = authorizer.Authorize(ctx, act, policy.ActionRead, object.RBACObject()) + err = authorizer.Authorize(ctx, act, action, object.RBACObject()) if err != nil { return empty, logNotAuthorizedError(ctx, logger, err) } @@ -430,6 +425,18 @@ func fetch[ } } +func fetch[ + ArgumentType any, + ObjectType rbac.Objecter, + DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), +]( + logger slog.Logger, + authorizer rbac.Authorizer, + f DatabaseFunc, +) DatabaseFunc { + return fetchWithAction(logger, authorizer, policy.ActionRead, f) +} + // fetchAndExec uses fetchAndQuery but only returns the error. The naming comes // from SQL 'exec' functions which only return an error. // See fetchAndQuery for more information. @@ -502,6 +509,7 @@ func fetchWithPostFilter[ DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error), ]( authorizer rbac.Authorizer, + action policy.Action, f DatabaseFunc, ) DatabaseFunc { return func(ctx context.Context, arg ArgumentType) (empty []ObjectType, err error) { @@ -518,7 +526,7 @@ func fetchWithPostFilter[ } // Authorize the action - return rbac.Filter(ctx, authorizer, act, policy.ActionRead, objects) + return rbac.Filter(ctx, authorizer, act, action, objects) } } @@ -574,7 +582,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r return NoActorError } - roleAssign := rbac.ResourceRoleAssignment + roleAssign := rbac.ResourceAssignRole shouldBeOrgRoles := false if orgID != nil { roleAssign = roleAssign.InOrg(*orgID) @@ -745,7 +753,7 @@ func (q *querier) DeleteAPIKeyByID(ctx context.Context, id string) error { func (q *querier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error { // TODO: This is not 100% correct because it omits apikey IDs. err := q.authorizeContext(ctx, policy.ActionDelete, - rbac.ResourceAPIKey.WithOwner(userID.String())) + rbac.ResourceApiKey.WithOwner(userID.String())) if err != nil { return err } @@ -769,7 +777,7 @@ func (q *querier) DeleteAllTailnetTunnels(ctx context.Context, arg database.Dele func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error { // TODO: This is not 100% correct because it omits apikey IDs. err := q.authorizeContext(ctx, policy.ActionDelete, - rbac.ResourceAPIKey.WithOwner(userID.String())) + rbac.ResourceApiKey.WithOwner(userID.String())) if err != nil { return err } @@ -784,14 +792,14 @@ func (q *querier) DeleteCoordinator(ctx context.Context, id uuid.UUID) error { } func (q *querier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error { - return deleteQ(q.log, q.auth, func(ctx context.Context, arg database.DeleteExternalAuthLinkParams) (database.ExternalAuthLink, error) { + return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, func(ctx context.Context, arg database.DeleteExternalAuthLinkParams) (database.ExternalAuthLink, error) { //nolint:gosimple return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID}) }, q.db.DeleteExternalAuthLink)(ctx, arg) } func (q *querier) DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error { - return deleteQ(q.log, q.auth, q.db.GetGitSSHKey, q.db.DeleteGitSSHKey)(ctx, userID) + return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, q.db.GetGitSSHKey, q.db.DeleteGitSSHKey)(ctx, userID) } func (q *querier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error { @@ -818,7 +826,7 @@ func (q *querier) DeleteLicense(ctx context.Context, id int32) (int32, error) { } func (q *querier) DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOAuth2ProviderApp); err != nil { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOauth2App); err != nil { return err } return q.db.DeleteOAuth2ProviderAppByID(ctx, id) @@ -964,15 +972,15 @@ func (q *querier) GetAPIKeyByName(ctx context.Context, arg database.GetAPIKeyByN } func (q *querier) GetAPIKeysByLoginType(ctx context.Context, loginType database.LoginType) ([]database.APIKey, error) { - return fetchWithPostFilter(q.auth, q.db.GetAPIKeysByLoginType)(ctx, loginType) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysByLoginType)(ctx, loginType) } func (q *querier) GetAPIKeysByUserID(ctx context.Context, params database.GetAPIKeysByUserIDParams) ([]database.APIKey, error) { - return fetchWithPostFilter(q.auth, q.db.GetAPIKeysByUserID)(ctx, database.GetAPIKeysByUserIDParams{LoginType: params.LoginType, UserID: params.UserID}) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysByUserID)(ctx, database.GetAPIKeysByUserIDParams{LoginType: params.LoginType, UserID: params.UserID}) } func (q *querier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]database.APIKey, error) { - return fetchWithPostFilter(q.auth, q.db.GetAPIKeysLastUsedAfter)(ctx, lastUsed) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysLastUsedAfter)(ctx, lastUsed) } func (q *querier) GetActiveUserCount(ctx context.Context) (int64, error) { @@ -1092,11 +1100,11 @@ func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.Get } func (q *querier) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) { - return fetch(q.log, q.auth, q.db.GetExternalAuthLink)(ctx, arg) + return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLink)(ctx, arg) } func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]database.ExternalAuthLink, error) { - return fetchWithPostFilter(q.auth, q.db.GetExternalAuthLinksByUserID)(ctx, userID) + return fetchWithPostFilter(q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLinksByUserID)(ctx, userID) } func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { @@ -1139,7 +1147,7 @@ func (q *querier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]dat } func (q *querier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) { - return fetch(q.log, q.auth, q.db.GetGitSSHKey)(ctx, userID) + return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetGitSSHKey)(ctx, userID) } func (q *querier) GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error) { @@ -1158,11 +1166,11 @@ func (q *querier) GetGroupMembers(ctx context.Context, id uuid.UUID) ([]database } func (q *querier) GetGroupsByOrganizationAndUserID(ctx context.Context, arg database.GetGroupsByOrganizationAndUserIDParams) ([]database.Group, error) { - return fetchWithPostFilter(q.auth, q.db.GetGroupsByOrganizationAndUserID)(ctx, arg) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupsByOrganizationAndUserID)(ctx, arg) } func (q *querier) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.Group, error) { - return fetchWithPostFilter(q.auth, q.db.GetGroupsByOrganizationID)(ctx, organizationID) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupsByOrganizationID)(ctx, organizationID) } func (q *querier) GetHealthSettings(ctx context.Context) (string, error) { @@ -1227,7 +1235,7 @@ func (q *querier) GetLicenses(ctx context.Context) ([]database.License, error) { fetch := func(ctx context.Context, _ interface{}) ([]database.License, error) { return q.db.GetLicenses(ctx) } - return fetchWithPostFilter(q.auth, fetch)(ctx, nil) + return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } func (q *querier) GetLogoURL(ctx context.Context) (string, error) { @@ -1323,7 +1331,7 @@ func (q *querier) GetOrganizationByName(ctx context.Context, name string) (datab func (q *querier) GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]database.GetOrganizationIDsByMemberIDsRow, error) { // TODO: This should be rewritten to return a list of database.OrganizationMember for consistent RBAC objects. // Currently this row returns a list of org ids per user, which is challenging to check against the RBAC system. - return fetchWithPostFilter(q.auth, q.db.GetOrganizationIDsByMemberIDs)(ctx, ids) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetOrganizationIDsByMemberIDs)(ctx, ids) } func (q *querier) GetOrganizationMemberByUserID(ctx context.Context, arg database.GetOrganizationMemberByUserIDParams) (database.OrganizationMember, error) { @@ -1331,18 +1339,18 @@ func (q *querier) GetOrganizationMemberByUserID(ctx context.Context, arg databas } func (q *querier) GetOrganizationMembershipsByUserID(ctx context.Context, userID uuid.UUID) ([]database.OrganizationMember, error) { - return fetchWithPostFilter(q.auth, q.db.GetOrganizationMembershipsByUserID)(ctx, userID) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetOrganizationMembershipsByUserID)(ctx, userID) } func (q *querier) GetOrganizations(ctx context.Context) ([]database.Organization, error) { fetch := func(ctx context.Context, _ interface{}) ([]database.Organization, error) { return q.db.GetOrganizations(ctx) } - return fetchWithPostFilter(q.auth, fetch)(ctx, nil) + return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } func (q *querier) GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]database.Organization, error) { - return fetchWithPostFilter(q.auth, q.db.GetOrganizationsByUserID)(ctx, userID) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetOrganizationsByUserID)(ctx, userID) } func (q *querier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) { @@ -1384,7 +1392,7 @@ func (q *querier) GetProvisionerDaemons(ctx context.Context) ([]database.Provisi fetch := func(ctx context.Context, _ interface{}) ([]database.ProvisionerDaemon, error) { return q.db.GetProvisionerDaemons(ctx) } - return fetchWithPostFilter(q.auth, fetch)(ctx, nil) + return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { @@ -1843,7 +1851,11 @@ func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params da if err != nil { return nil, err } - if err := q.authorizeContext(ctx, policy.ActionRead, u.UserWorkspaceBuildParametersObject()); err != nil { + // This permission is a bit strange. Reading workspace build params should be a permission + // on the workspace. However, this use case is to autofill a user's last input + // to some parameter. So this is kind of a "user setting". For now, this will + // be lumped in with user personal data. Subject to change. + if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { return nil, err } return q.db.GetUserWorkspaceBuildParameters(ctx, params) @@ -2100,7 +2112,7 @@ func (q *querier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceApp } func (q *querier) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { - return fetchWithPostFilter(q.auth, func(ctx context.Context, _ interface{}) ([]database.WorkspaceProxy, error) { + return fetchWithPostFilter(q.auth, policy.ActionRead, func(ctx context.Context, _ interface{}) ([]database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxies(ctx) })(ctx, nil) } @@ -2518,12 +2530,12 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW return xerrors.Errorf("get workspace by id: %w", err) } - var action policy.Action = policy.ActionUpdate + var action policy.Action = policy.ActionWorkspaceBuild if arg.Transition == database.WorkspaceTransitionDelete { action = policy.ActionDelete } - if err = q.authorizeContext(ctx, action, w.WorkspaceBuildRBAC(arg.Transition)); err != nil { + if err = q.authorizeContext(ctx, action, w); err != nil { return xerrors.Errorf("authorize context: %w", err) } @@ -2676,14 +2688,14 @@ func (q *querier) UpdateExternalAuthLink(ctx context.Context, arg database.Updat fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) { return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID}) } - return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateExternalAuthLink)(ctx, arg) + return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateExternalAuthLink)(ctx, arg) } func (q *querier) UpdateGitSSHKey(ctx context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) { fetch := func(ctx context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) { return q.db.GetGitSSHKey(ctx, arg.UserID) } - return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateGitSSHKey)(ctx, arg) + return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateGitSSHKey)(ctx, arg) } func (q *querier) UpdateGroupByID(ctx context.Context, arg database.UpdateGroupByIDParams) (database.Group, error) { @@ -2995,7 +3007,7 @@ func (q *querier) UpdateUserLink(ctx context.Context, arg database.UpdateUserLin LoginType: arg.LoginType, }) } - return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserLink)(ctx, arg) + return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateUserLink)(ctx, arg) } func (q *querier) UpdateUserLinkedID(ctx context.Context, arg database.UpdateUserLinkedIDParams) (database.UserLink, error) { @@ -3017,7 +3029,7 @@ func (q *querier) UpdateUserProfile(ctx context.Context, arg database.UpdateUser if err != nil { return database.User{}, err } - if err := q.authorizeContext(ctx, policy.ActionUpdate, u.UserDataRBACObject()); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.User{}, err } return q.db.UpdateUserProfile(ctx, arg) @@ -3028,7 +3040,7 @@ func (q *querier) UpdateUserQuietHoursSchedule(ctx context.Context, arg database if err != nil { return database.User{}, err } - if err := q.authorizeContext(ctx, policy.ActionUpdate, u.UserDataRBACObject()); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.User{}, err } return q.db.UpdateUserQuietHoursSchedule(ctx, arg) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 92dbbb8e7bce1..6d8dfe82f9c53 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -218,7 +218,7 @@ func (s *MethodTestSuite) TestAPIKey() { UserID: u.ID, LoginType: database.LoginTypePassword, Scope: database.APIKeyScopeAll, - }).Asserts(rbac.ResourceAPIKey.WithOwner(u.ID.String()), policy.ActionCreate) + }).Asserts(rbac.ResourceApiKey.WithOwner(u.ID.String()), policy.ActionCreate) })) s.Run("UpdateAPIKeyByID", s.Subtest(func(db database.Store, check *expects) { a, _ := dbgen.APIKey(s.T(), db, database.APIKey{}) @@ -230,21 +230,23 @@ func (s *MethodTestSuite) TestAPIKey() { a, _ := dbgen.APIKey(s.T(), db, database.APIKey{ Scope: database.APIKeyScopeApplicationConnect, }) - check.Args(a.UserID).Asserts(rbac.ResourceAPIKey.WithOwner(a.UserID.String()), policy.ActionDelete).Returns() + check.Args(a.UserID).Asserts(rbac.ResourceApiKey.WithOwner(a.UserID.String()), policy.ActionDelete).Returns() })) s.Run("DeleteExternalAuthLink", s.Subtest(func(db database.Store, check *expects) { a := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{}) check.Args(database.DeleteExternalAuthLinkParams{ ProviderID: a.ProviderID, UserID: a.UserID, - }).Asserts(a, policy.ActionDelete).Returns() + }).Asserts(rbac.ResourceUserObject(a.UserID), policy.ActionUpdatePersonal).Returns() })) s.Run("GetExternalAuthLinksByUserID", s.Subtest(func(db database.Store, check *expects) { a := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{}) b := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{ UserID: a.UserID, }) - check.Args(a.UserID).Asserts(a, policy.ActionRead, b, policy.ActionRead) + check.Args(a.UserID).Asserts( + rbac.ResourceUserObject(a.UserID), policy.ActionReadPersonal, + rbac.ResourceUserObject(b.UserID), policy.ActionReadPersonal) })) } @@ -524,10 +526,10 @@ func (s *MethodTestSuite) TestLicense() { Asserts(rbac.ResourceLicense, policy.ActionCreate) })) s.Run("UpsertLogoURL", s.Subtest(func(db database.Store, check *expects) { - check.Args("value").Asserts(rbac.ResourceDeploymentValues, policy.ActionCreate) + check.Args("value").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) })) s.Run("UpsertNotificationBanners", s.Subtest(func(db database.Store, check *expects) { - check.Args("value").Asserts(rbac.ResourceDeploymentValues, policy.ActionCreate) + check.Args("value").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) })) s.Run("GetLicenseByID", s.Subtest(func(db database.Store, check *expects) { l, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{ @@ -634,7 +636,7 @@ func (s *MethodTestSuite) TestOrganization() { UserID: u.ID, Roles: []string{rbac.RoleOrgAdmin(o.ID)}, }).Asserts( - rbac.ResourceRoleAssignment.InOrg(o.ID), policy.ActionCreate, + rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionCreate, rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { @@ -654,8 +656,8 @@ func (s *MethodTestSuite) TestOrganization() { OrgID: o.ID, }).Asserts( mem, policy.ActionRead, - rbac.ResourceRoleAssignment.InOrg(o.ID), policy.ActionCreate, // org-mem - rbac.ResourceRoleAssignment.InOrg(o.ID), policy.ActionDelete, // org-admin + rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionCreate, // org-mem + rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionDelete, // org-admin ).Returns(out) })) } @@ -942,31 +944,31 @@ func (s *MethodTestSuite) TestTemplate() { }).Asserts(t1, policy.ActionUpdate).Returns() })) s.Run("GetTemplateInsights", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateInsightsParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead) + check.Args(database.GetTemplateInsightsParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) s.Run("GetUserLatencyInsights", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetUserLatencyInsightsParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead) + check.Args(database.GetUserLatencyInsightsParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) s.Run("GetUserActivityInsights", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetUserActivityInsightsParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead).Errors(sql.ErrNoRows) + check.Args(database.GetUserActivityInsightsParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights).Errors(sql.ErrNoRows) })) s.Run("GetTemplateParameterInsights", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateParameterInsightsParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead) + check.Args(database.GetTemplateParameterInsightsParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) s.Run("GetTemplateInsightsByInterval", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateInsightsByIntervalParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead) + check.Args(database.GetTemplateInsightsByIntervalParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) s.Run("GetTemplateInsightsByTemplate", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateInsightsByTemplateParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead) + check.Args(database.GetTemplateInsightsByTemplateParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) s.Run("GetTemplateAppInsights", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateAppInsightsParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead) + check.Args(database.GetTemplateAppInsightsParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) s.Run("GetTemplateAppInsightsByTemplate", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateAppInsightsByTemplateParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead) + check.Args(database.GetTemplateAppInsightsByTemplateParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) s.Run("GetTemplateUsageStats", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateUsageStatsParams{}).Asserts(rbac.ResourceTemplateInsights, policy.ActionRead).Errors(sql.ErrNoRows) + check.Args(database.GetTemplateUsageStatsParams{}).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights).Errors(sql.ErrNoRows) })) s.Run("UpsertTemplateUsageStats", s.Subtest(func(db database.Store, check *expects) { check.Asserts(rbac.ResourceSystem, policy.ActionUpdate) @@ -982,7 +984,7 @@ func (s *MethodTestSuite) TestUser() { })) s.Run("DeleteAPIKeysByUserID", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) - check.Args(u.ID).Asserts(rbac.ResourceAPIKey.WithOwner(u.ID.String()), policy.ActionDelete).Returns() + check.Args(u.ID).Asserts(rbac.ResourceApiKey.WithOwner(u.ID.String()), policy.ActionDelete).Returns() })) s.Run("GetQuotaAllowanceForUser", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) @@ -1021,7 +1023,7 @@ func (s *MethodTestSuite) TestUser() { check.Args(database.InsertUserParams{ ID: uuid.New(), LoginType: database.LoginTypePassword, - }).Asserts(rbac.ResourceRoleAssignment, policy.ActionCreate, rbac.ResourceUser, policy.ActionCreate) + }).Asserts(rbac.ResourceAssignRole, policy.ActionCreate, rbac.ResourceUser, policy.ActionCreate) })) s.Run("InsertUserLink", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) @@ -1038,13 +1040,13 @@ func (s *MethodTestSuite) TestUser() { u := dbgen.User(s.T(), db, database.User{}) check.Args(database.UpdateUserHashedPasswordParams{ ID: u.ID, - }).Asserts(u.UserDataRBACObject(), policy.ActionUpdate).Returns() + }).Asserts(u, policy.ActionUpdatePersonal).Returns() })) s.Run("UpdateUserQuietHoursSchedule", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) check.Args(database.UpdateUserQuietHoursScheduleParams{ ID: u.ID, - }).Asserts(u.UserDataRBACObject(), policy.ActionUpdate) + }).Asserts(u, policy.ActionUpdatePersonal) })) s.Run("UpdateUserLastSeenAt", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) @@ -1061,7 +1063,7 @@ func (s *MethodTestSuite) TestUser() { Email: u.Email, Username: u.Username, UpdatedAt: u.UpdatedAt, - }).Asserts(u.UserDataRBACObject(), policy.ActionUpdate).Returns(u) + }).Asserts(u, policy.ActionUpdatePersonal).Returns(u) })) s.Run("GetUserWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) @@ -1070,7 +1072,7 @@ func (s *MethodTestSuite) TestUser() { OwnerID: u.ID, TemplateID: uuid.UUID{}, }, - ).Asserts(u.UserWorkspaceBuildParametersObject(), policy.ActionRead).Returns( + ).Asserts(u, policy.ActionReadPersonal).Returns( []database.GetUserWorkspaceBuildParametersRow{}, ) })) @@ -1080,7 +1082,7 @@ func (s *MethodTestSuite) TestUser() { ID: u.ID, ThemePreference: u.ThemePreference, UpdatedAt: u.UpdatedAt, - }).Asserts(u.UserDataRBACObject(), policy.ActionUpdate).Returns(u) + }).Asserts(u, policy.ActionUpdatePersonal).Returns(u) })) s.Run("UpdateUserStatus", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) @@ -1092,38 +1094,38 @@ func (s *MethodTestSuite) TestUser() { })) s.Run("DeleteGitSSHKey", s.Subtest(func(db database.Store, check *expects) { key := dbgen.GitSSHKey(s.T(), db, database.GitSSHKey{}) - check.Args(key.UserID).Asserts(key, policy.ActionDelete).Returns() + check.Args(key.UserID).Asserts(rbac.ResourceUserObject(key.UserID), policy.ActionUpdatePersonal).Returns() })) s.Run("GetGitSSHKey", s.Subtest(func(db database.Store, check *expects) { key := dbgen.GitSSHKey(s.T(), db, database.GitSSHKey{}) - check.Args(key.UserID).Asserts(key, policy.ActionRead).Returns(key) + check.Args(key.UserID).Asserts(rbac.ResourceUserObject(key.UserID), policy.ActionReadPersonal).Returns(key) })) s.Run("InsertGitSSHKey", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) check.Args(database.InsertGitSSHKeyParams{ UserID: u.ID, - }).Asserts(rbac.ResourceUserData.WithID(u.ID).WithOwner(u.ID.String()), policy.ActionCreate) + }).Asserts(u, policy.ActionUpdatePersonal) })) s.Run("UpdateGitSSHKey", s.Subtest(func(db database.Store, check *expects) { key := dbgen.GitSSHKey(s.T(), db, database.GitSSHKey{}) check.Args(database.UpdateGitSSHKeyParams{ UserID: key.UserID, UpdatedAt: key.UpdatedAt, - }).Asserts(key, policy.ActionUpdate).Returns(key) + }).Asserts(rbac.ResourceUserObject(key.UserID), policy.ActionUpdatePersonal).Returns(key) })) s.Run("GetExternalAuthLink", s.Subtest(func(db database.Store, check *expects) { link := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{}) check.Args(database.GetExternalAuthLinkParams{ ProviderID: link.ProviderID, UserID: link.UserID, - }).Asserts(link, policy.ActionRead).Returns(link) + }).Asserts(rbac.ResourceUserObject(link.UserID), policy.ActionReadPersonal).Returns(link) })) s.Run("InsertExternalAuthLink", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) check.Args(database.InsertExternalAuthLinkParams{ ProviderID: uuid.NewString(), UserID: u.ID, - }).Asserts(rbac.ResourceUserData.WithOwner(u.ID.String()).WithID(u.ID), policy.ActionCreate) + }).Asserts(u, policy.ActionUpdatePersonal) })) s.Run("UpdateExternalAuthLink", s.Subtest(func(db database.Store, check *expects) { link := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{}) @@ -1134,7 +1136,7 @@ func (s *MethodTestSuite) TestUser() { OAuthRefreshToken: link.OAuthRefreshToken, OAuthExpiry: link.OAuthExpiry, UpdatedAt: link.UpdatedAt, - }).Asserts(link, policy.ActionUpdate).Returns(link) + }).Asserts(rbac.ResourceUserObject(link.UserID), policy.ActionUpdatePersonal).Returns(link) })) s.Run("UpdateUserLink", s.Subtest(func(db database.Store, check *expects) { link := dbgen.UserLink(s.T(), db, database.UserLink{}) @@ -1145,7 +1147,7 @@ func (s *MethodTestSuite) TestUser() { UserID: link.UserID, LoginType: link.LoginType, DebugContext: json.RawMessage("{}"), - }).Asserts(link, policy.ActionUpdate).Returns(link) + }).Asserts(rbac.ResourceUserObject(link.UserID), policy.ActionUpdatePersonal).Returns(link) })) s.Run("UpdateUserRoles", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{RBACRoles: []string{rbac.RoleTemplateAdmin()}}) @@ -1156,8 +1158,8 @@ func (s *MethodTestSuite) TestUser() { ID: u.ID, }).Asserts( u, policy.ActionRead, - rbac.ResourceRoleAssignment, policy.ActionCreate, - rbac.ResourceRoleAssignment, policy.ActionDelete, + rbac.ResourceAssignRole, policy.ActionCreate, + rbac.ResourceAssignRole, policy.ActionDelete, ).Returns(o) })) s.Run("AllUserIDs", s.Subtest(func(db database.Store, check *expects) { @@ -1430,7 +1432,7 @@ func (s *MethodTestSuite) TestWorkspace() { WorkspaceID: w.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, - }).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), policy.ActionUpdate) + }).Asserts(w, policy.ActionWorkspaceBuild) })) s.Run("Start/RequireActiveVersion/VersionMismatch/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { t := dbgen.Template(s.T(), db, database.Template{}) @@ -1452,7 +1454,7 @@ func (s *MethodTestSuite) TestWorkspace() { Reason: database.BuildReasonInitiator, TemplateVersionID: v.ID, }).Asserts( - w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), policy.ActionUpdate, + w, policy.ActionWorkspaceBuild, t, policy.ActionUpdate, ) })) @@ -1480,7 +1482,7 @@ func (s *MethodTestSuite) TestWorkspace() { Reason: database.BuildReasonInitiator, TemplateVersionID: v.ID, }).Asserts( - w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), policy.ActionUpdate, + w, policy.ActionWorkspaceBuild, ) })) s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { @@ -1489,7 +1491,7 @@ func (s *MethodTestSuite) TestWorkspace() { WorkspaceID: w.ID, Transition: database.WorkspaceTransitionDelete, Reason: database.BuildReasonInitiator, - }).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionDelete), policy.ActionDelete) + }).Asserts(w, policy.ActionDelete) })) s.Run("InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) { w := dbgen.Workspace(s.T(), db, database.Workspace{}) @@ -2204,13 +2206,13 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args().Asserts() })) s.Run("UpsertApplicationName", s.Subtest(func(db database.Store, check *expects) { - check.Args("").Asserts(rbac.ResourceDeploymentValues, policy.ActionCreate) + check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) })) s.Run("GetHealthSettings", s.Subtest(func(db database.Store, check *expects) { check.Args().Asserts() })) s.Run("UpsertHealthSettings", s.Subtest(func(db database.Store, check *expects) { - check.Args("foo").Asserts(rbac.ResourceDeploymentValues, policy.ActionCreate) + check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) })) s.Run("GetDeploymentWorkspaceAgentStats", s.Subtest(func(db database.Store, check *expects) { check.Args(time.Time{}).Asserts() @@ -2335,11 +2337,11 @@ func (s *MethodTestSuite) TestOAuth2ProviderApps() { dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{Name: "first"}), dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{Name: "last"}), } - check.Args().Asserts(rbac.ResourceOAuth2ProviderApp, policy.ActionRead).Returns(apps) + check.Args().Asserts(rbac.ResourceOauth2App, policy.ActionRead).Returns(apps) })) s.Run("GetOAuth2ProviderAppByID", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) - check.Args(app.ID).Asserts(rbac.ResourceOAuth2ProviderApp, policy.ActionRead).Returns(app) + check.Args(app.ID).Asserts(rbac.ResourceOauth2App, policy.ActionRead).Returns(app) })) s.Run("GetOAuth2ProviderAppsByUserID", s.Subtest(func(db database.Store, check *expects) { user := dbgen.User(s.T(), db, database.User{}) @@ -2357,7 +2359,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderApps() { APIKeyID: key.ID, }) } - check.Args(user.ID).Asserts(rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(user.ID.String()), policy.ActionRead).Returns([]database.GetOAuth2ProviderAppsByUserIDRow{ + check.Args(user.ID).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionRead).Returns([]database.GetOAuth2ProviderAppsByUserIDRow{ { OAuth2ProviderApp: database.OAuth2ProviderApp{ ID: app.ID, @@ -2370,7 +2372,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderApps() { }) })) s.Run("InsertOAuth2ProviderApp", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertOAuth2ProviderAppParams{}).Asserts(rbac.ResourceOAuth2ProviderApp, policy.ActionCreate) + check.Args(database.InsertOAuth2ProviderAppParams{}).Asserts(rbac.ResourceOauth2App, policy.ActionCreate) })) s.Run("UpdateOAuth2ProviderAppByID", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) @@ -2381,11 +2383,11 @@ func (s *MethodTestSuite) TestOAuth2ProviderApps() { Name: app.Name, CallbackURL: app.CallbackURL, UpdatedAt: app.UpdatedAt, - }).Asserts(rbac.ResourceOAuth2ProviderApp, policy.ActionUpdate).Returns(app) + }).Asserts(rbac.ResourceOauth2App, policy.ActionUpdate).Returns(app) })) s.Run("DeleteOAuth2ProviderAppByID", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) - check.Args(app.ID).Asserts(rbac.ResourceOAuth2ProviderApp, policy.ActionDelete) + check.Args(app.ID).Asserts(rbac.ResourceOauth2App, policy.ActionDelete) })) } @@ -2405,27 +2407,27 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppSecrets() { _ = dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{ AppID: app2.ID, }) - check.Args(app1.ID).Asserts(rbac.ResourceOAuth2ProviderAppSecret, policy.ActionRead).Returns(secrets) + check.Args(app1.ID).Asserts(rbac.ResourceOauth2AppSecret, policy.ActionRead).Returns(secrets) })) s.Run("GetOAuth2ProviderAppSecretByID", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) secret := dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{ AppID: app.ID, }) - check.Args(secret.ID).Asserts(rbac.ResourceOAuth2ProviderAppSecret, policy.ActionRead).Returns(secret) + check.Args(secret.ID).Asserts(rbac.ResourceOauth2AppSecret, policy.ActionRead).Returns(secret) })) s.Run("GetOAuth2ProviderAppSecretByPrefix", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) secret := dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{ AppID: app.ID, }) - check.Args(secret.SecretPrefix).Asserts(rbac.ResourceOAuth2ProviderAppSecret, policy.ActionRead).Returns(secret) + check.Args(secret.SecretPrefix).Asserts(rbac.ResourceOauth2AppSecret, policy.ActionRead).Returns(secret) })) s.Run("InsertOAuth2ProviderAppSecret", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) check.Args(database.InsertOAuth2ProviderAppSecretParams{ AppID: app.ID, - }).Asserts(rbac.ResourceOAuth2ProviderAppSecret, policy.ActionCreate) + }).Asserts(rbac.ResourceOauth2AppSecret, policy.ActionCreate) })) s.Run("UpdateOAuth2ProviderAppSecretByID", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) @@ -2436,14 +2438,14 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppSecrets() { check.Args(database.UpdateOAuth2ProviderAppSecretByIDParams{ ID: secret.ID, LastUsedAt: secret.LastUsedAt, - }).Asserts(rbac.ResourceOAuth2ProviderAppSecret, policy.ActionUpdate).Returns(secret) + }).Asserts(rbac.ResourceOauth2AppSecret, policy.ActionUpdate).Returns(secret) })) s.Run("DeleteOAuth2ProviderAppSecretByID", s.Subtest(func(db database.Store, check *expects) { app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{}) secret := dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{ AppID: app.ID, }) - check.Args(secret.ID).Asserts(rbac.ResourceOAuth2ProviderAppSecret, policy.ActionDelete) + check.Args(secret.ID).Asserts(rbac.ResourceOauth2AppSecret, policy.ActionDelete) })) } @@ -2472,7 +2474,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppCodes() { check.Args(database.InsertOAuth2ProviderAppCodeParams{ AppID: app.ID, UserID: user.ID, - }).Asserts(rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(user.ID.String()), policy.ActionCreate) + }).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionCreate) })) s.Run("DeleteOAuth2ProviderAppCodeByID", s.Subtest(func(db database.Store, check *expects) { user := dbgen.User(s.T(), db, database.User{}) @@ -2495,7 +2497,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppCodes() { check.Args(database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams{ AppID: app.ID, UserID: user.ID, - }).Asserts(rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(user.ID.String()), policy.ActionDelete) + }).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionDelete) })) } @@ -2512,7 +2514,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() { check.Args(database.InsertOAuth2ProviderAppTokenParams{ AppSecretID: secret.ID, APIKeyID: key.ID, - }).Asserts(rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(user.ID.String()), policy.ActionCreate) + }).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionCreate) })) s.Run("GetOAuth2ProviderAppTokenByPrefix", s.Subtest(func(db database.Store, check *expects) { user := dbgen.User(s.T(), db, database.User{}) @@ -2527,7 +2529,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() { AppSecretID: secret.ID, APIKeyID: key.ID, }) - check.Args(token.HashPrefix).Asserts(rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(user.ID.String()), policy.ActionRead) + check.Args(token.HashPrefix).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionRead) })) s.Run("DeleteOAuth2ProviderAppTokensByAppAndUserID", s.Subtest(func(db database.Store, check *expects) { user := dbgen.User(s.T(), db, database.User{}) @@ -2547,6 +2549,6 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() { check.Args(database.DeleteOAuth2ProviderAppTokensByAppAndUserIDParams{ AppID: app.ID, UserID: user.ID, - }).Asserts(rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(user.ID.String()), policy.ActionDelete) + }).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionDelete) })) } diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 13acaaaa77487..d71c63b089556 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -164,22 +164,6 @@ func (w Workspace) RBACObject() rbac.Object { WithOwner(w.OwnerID.String()) } -func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Object { - // If a workspace is dormant it cannot be built. - // However we need to allow stopping a workspace by a caller once a workspace - // is locked (e.g. for autobuild). Additionally, if a user wants to delete - // a locked workspace, they shouldn't have to have it unlocked first. - if w.DormantAt.Valid && transition != WorkspaceTransitionStop && - transition != WorkspaceTransitionDelete { - return w.DormantRBAC() - } - - return rbac.ResourceWorkspaceBuild. - WithID(w.ID). - InOrg(w.OrganizationID). - WithOwner(w.OwnerID.String()) -} - func (w Workspace) DormantRBAC() rbac.Object { return rbac.ResourceWorkspaceDormant. WithID(w.ID). @@ -227,32 +211,17 @@ func (f File) RBACObject() rbac.Object { } // RBACObject returns the RBAC object for the site wide user resource. -// If you are trying to get the RBAC object for the UserData, use -// u.UserDataRBACObject() instead. func (u User) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.ID) } -func (u User) UserDataRBACObject() rbac.Object { - return rbac.ResourceUserData.WithID(u.ID).WithOwner(u.ID.String()) -} - -func (u User) UserWorkspaceBuildParametersObject() rbac.Object { - return rbac.ResourceUserWorkspaceBuildParameters.WithID(u.ID).WithOwner(u.ID.String()) -} - func (u GetUsersRow) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.ID) } -func (u GitSSHKey) RBACObject() rbac.Object { - return rbac.ResourceUserData.WithID(u.UserID).WithOwner(u.UserID.String()) -} - -func (u ExternalAuthLink) RBACObject() rbac.Object { - // I assume UserData is ok? - return rbac.ResourceUserData.WithID(u.UserID).WithOwner(u.UserID.String()) -} +func (u GitSSHKey) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) } +func (u ExternalAuthLink) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) } +func (u UserLink) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) } func (u ExternalAuthLink) OAuthToken() *oauth2.Token { return &oauth2.Token{ @@ -262,11 +231,6 @@ func (u ExternalAuthLink) OAuthToken() *oauth2.Token { } } -func (u UserLink) RBACObject() rbac.Object { - // I assume UserData is ok? - return rbac.ResourceUserData.WithOwner(u.UserID.String()).WithID(u.UserID) -} - func (l License) RBACObject() rbac.Object { return rbac.ResourceLicense.WithIDString(strconv.FormatInt(int64(l.ID), 10)) } diff --git a/coderd/debug.go b/coderd/debug.go index 0e98539a71f75..b1f17f29e0102 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -194,7 +194,7 @@ func (api *API) deploymentHealthSettings(rw http.ResponseWriter, r *http.Request func (api *API) putDeploymentHealthSettings(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentValues) { + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "Insufficient permissions to update health settings.", }) diff --git a/coderd/deployment.go b/coderd/deployment.go index 572bf9076bb59..4c78563a80456 100644 --- a/coderd/deployment.go +++ b/coderd/deployment.go @@ -17,7 +17,7 @@ import ( // @Success 200 {object} codersdk.DeploymentConfig // @Router /deployment/config [get] func (api *API) deploymentValues(rw http.ResponseWriter, r *http.Request) { - if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentValues) { + if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig) { httpapi.Forbidden(rw) return } diff --git a/coderd/insights.go b/coderd/insights.go index 85b4ec8661d9c..2da27e2561762 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -33,7 +33,7 @@ const insightsTimeLayout = time.RFC3339 // @Success 200 {object} codersdk.DAUsResponse // @Router /insights/daus [get] func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) { - if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentValues) { + if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig) { httpapi.Forbidden(rw) return } diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 28d79a261e289..1c66c4f6caf3e 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -1,6 +1,8 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac +import "github.com/coder/coder/v2/coderd/rbac/policy" + // Objecter returns the RBAC object for itself. type Objecter interface { RBACObject() Object @@ -262,3 +264,20 @@ func AllResources() []Objecter { ResourceWorkspaceProxy, } } + +func AllActions() []policy.Action { + return []policy.Action{ + policy.ActionUpdate, + policy.ActionAssign, + policy.ActionReadPersonal, + policy.ActionCreate, + policy.ActionUse, + policy.ActionApplicationConnect, + policy.ActionViewInsights, + policy.ActionUpdatePersonal, + policy.ActionRead, + policy.ActionDelete, + policy.ActionWorkspaceBuild, + policy.ActionSSH, + } +} diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 3e79253e83e82..3dc2904911d8b 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -20,8 +20,7 @@ const ( ActionApplicationConnect Action = "application_connect" ActionViewInsights Action = "view_insights" - ActionWorkspaceBuild Action = "build" - ActionViewWorkspaceBuildParams Action = "build_parameters" + ActionWorkspaceBuild Action = "build" ActionAssign Action = "assign" @@ -101,7 +100,7 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionReadPersonal: actDef(fieldOwner, "read personal user data like password"), ActionUpdatePersonal: actDef(fieldOwner, "update personal data"), - //ActionReadPublic: actDef(fieldOwner, "read public user data"), + // ActionReadPublic: actDef(fieldOwner, "read public user data"), }, }, "workspace": { @@ -114,9 +113,6 @@ var RBACPermissions = map[string]PermissionDefinition{ // Workspace provisioning ActionWorkspaceBuild: actDef(fieldOwner|fieldOrg|fieldACL, "allows starting, stopping, and updating a workspace"), - // TODO: ActionViewWorkspaceBuildParams is very werid. Seems to be used for autofilling the last params set. - // Admins want this so they can update a user's workspace with the old values?? - ActionViewWorkspaceBuildParams: actDef(fieldOwner|fieldOrg|fieldACL, "view workspace build parameters"), // Running a workspace ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"), diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index f0e660a698df5..4e58415ae924b 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -166,8 +166,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Permissions(map[string][]policy.Action{ // Users cannot do create/update/delete on themselves, but they // can read their own details. - ResourceUser.Type: {policy.ActionRead}, - ResourceWorkspace.Type: {policy.ActionViewWorkspaceBuildParams}, + ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, // Users can create provisioner daemons scoped to themselves. ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, })..., @@ -224,7 +223,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) { policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionUpdatePersonal, policy.ActionReadPersonal, }, - ResourceWorkspace.Type: {policy.ActionViewWorkspaceBuildParams}, // Full perms to manage org members ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, diff --git a/coderd/roles.go b/coderd/roles.go index 1cc74535119e3..ac74dd1c91311 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -23,7 +23,7 @@ import ( func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() actorRoles := httpmw.UserAuthorization(r) - if !api.Authorize(r, policy.ActionRead, rbac.ResourceRoleAssignment) { + if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig) { httpapi.Forbidden(rw) return } @@ -47,7 +47,7 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) actorRoles := httpmw.UserAuthorization(r) - if !api.Authorize(r, policy.ActionRead, rbac.ResourceOrgRoleAssignment.InOrg(organization.ID)) { + if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig.InOrg(organization.ID)) { httpapi.ResourceNotFound(rw) return } diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 3959c0e55a428..b34eb9ce3c858 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -665,7 +665,7 @@ func (b *Builder) authorize(authFunc func(action policy.Action, object rbac.Obje } } - if b.logLevel != "" && !authFunc(policy.ActionRead, rbac.ResourceDeploymentValues) { + if b.logLevel != "" && !authFunc(policy.ActionRead, rbac.ResourceDeploymentConfig) { return BuildError{ http.StatusBadRequest, "Workspace builds with a custom log level are restricted to administrators only.", diff --git a/enterprise/coderd/appearance.go b/enterprise/coderd/appearance.go index 5104936ac62a4..8a9d51cdb9070 100644 --- a/enterprise/coderd/appearance.go +++ b/enterprise/coderd/appearance.go @@ -137,7 +137,7 @@ func validateHexColor(color string) error { func (api *API) putAppearance(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentValues) { + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "Insufficient permissions to update appearance", }) diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 6affc14e900cd..9f62fa249feda 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -121,6 +121,13 @@ func generate(ctx context.Context) ([]byte, error) { tpl, err := template.New("object.gotmpl").Funcs(template.FuncMap{ "capitalize": capitalize, "pascalCaseName": pascalCaseName[string], + "actionsList": func() []string { + tmp := make([]string, 0) + for _, actionEnum := range actionMap { + tmp = append(tmp, actionEnum) + } + return tmp + }, "actionEnum": func(action policy.Action) string { x++ v, ok := actionMap[string(action)] diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/object.gotmpl index af766cb57d6fd..b005955e88dba 100644 --- a/scripts/rbacgen/object.gotmpl +++ b/scripts/rbacgen/object.gotmpl @@ -1,6 +1,8 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac +import "github.com/coder/coder/v2/coderd/rbac/policy" + // Objecter returns the RBAC object for itself. type Objecter interface { RBACObject() Object @@ -27,3 +29,11 @@ func AllResources() []Objecter { {{- end }} } } + +func AllActions() []policy.Action { + return []policy.Action { + {{- range $element := actionsList }} + policy.{{ $element }}, + {{- end }} + } +} From c70c5268589b0ec851b6ce815b04d5c59d0a79a4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 18:01:23 -0500 Subject: [PATCH 13/24] Update roles --- coderd/rbac/object_gen.go | 17 ++++++++--------- coderd/rbac/object_test.go | 10 +++++----- coderd/rbac/policy/policy.go | 1 + 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 1c66c4f6caf3e..65baa1a0077f4 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -208,7 +208,6 @@ var ( // Valid Actions // - "ActionApplicationConnect" needs [owner,org,acl] :: connect to workspace apps via browser // - "ActionWorkspaceBuild" needs [owner,org,acl] :: allows starting, stopping, and updating a workspace - // - "ActionViewWorkspaceBuildParams" needs [owner,org,acl] :: view workspace build parameters // - "ActionCreate" needs [owner,org] :: create a new workspace // - "ActionDelete" needs [owner,org,acl] :: delete workspace // - "ActionRead" needs [owner,org,acl] :: read workspace data to view on the UI @@ -267,17 +266,17 @@ func AllResources() []Objecter { func AllActions() []policy.Action { return []policy.Action{ - policy.ActionUpdate, - policy.ActionAssign, - policy.ActionReadPersonal, policy.ActionCreate, - policy.ActionUse, + policy.ActionRead, + policy.ActionSSH, policy.ActionApplicationConnect, policy.ActionViewInsights, - policy.ActionUpdatePersonal, - policy.ActionRead, - policy.ActionDelete, policy.ActionWorkspaceBuild, - policy.ActionSSH, + policy.ActionAssign, + policy.ActionUpdate, + policy.ActionDelete, + policy.ActionUse, + policy.ActionReadPersonal, + policy.ActionUpdatePersonal, } } diff --git a/coderd/rbac/object_test.go b/coderd/rbac/object_test.go index 373119f7f0e57..ea6031f2ccae8 100644 --- a/coderd/rbac/object_test.go +++ b/coderd/rbac/object_test.go @@ -184,14 +184,14 @@ func TestAllResources(t *testing.T) { var typeNames []string resources := rbac.AllResources() for _, r := range resources { - if r.Type == "" { - t.Errorf("empty type name: %s", r.Type) + if r.RBACObject().Type == "" { + t.Errorf("empty type name: %s", r.RBACObject().Type) continue } - if slice.Contains(typeNames, r.Type) { - t.Errorf("duplicate type name: %s", r.Type) + if slice.Contains(typeNames, r.RBACObject().Type) { + t.Errorf("duplicate type name: %s", r.RBACObject().Type) continue } - typeNames = append(typeNames, r.Type) + typeNames = append(typeNames, r.RBACObject().Type) } } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 3dc2904911d8b..b216235698735 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -244,6 +244,7 @@ var RBACPermissions = map[string]PermissionDefinition{ "assign_org_role": { Actions: map[Action]ActionDefinition{ ActionAssign: actDef(0, "ability to assign org scoped roles"), + ActionRead: actDef(0, "view what roles are assignable"), ActionDelete: actDef(0, "ability to delete org scoped roles"), }, }, From 66107d3ddf5152a821100ea622eaecbcfce5ed88 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 18:25:36 -0500 Subject: [PATCH 14/24] update roles_test to include all actions and resources --- coderd/rbac/roles.go | 3 +- coderd/rbac/roles_test.go | 234 ++++++++++++++++++++++++++++++++++---- 2 files changed, 212 insertions(+), 25 deletions(-) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 4e58415ae924b..8744eb05a7173 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -159,7 +159,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // All users can see the provisioner daemons. ResourceProvisionerDaemon.Type: {policy.ActionRead}, // All users can see OAuth2 provider applications. - ResourceOauth2App.Type: {policy.ActionRead}, + ResourceOauth2App.Type: {policy.ActionRead}, + ResourceWorkspaceProxy.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember), diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index b5e78e606b8d4..ea237aa560dfd 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -36,8 +36,8 @@ func TestOwnerExec(t *testing.T) { auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) // Exec a random workspace - err := auth.Authorize(context.Background(), owner, policy.ActionCreate, - rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString())) + err := auth.Authorize(context.Background(), owner, policy.ActionSSH, + rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString())) require.ErrorAsf(t, err, &rbac.UnauthorizedError{}, "expected unauthorized error") }) @@ -50,8 +50,8 @@ func TestOwnerExec(t *testing.T) { auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) // Exec a random workspace - err := auth.Authorize(context.Background(), owner, policy.ActionCreate, - rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString())) + err := auth.Authorize(context.Background(), owner, policy.ActionSSH, + rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString())) require.NoError(t, err, "expected owner can") }) } @@ -60,6 +60,8 @@ func TestOwnerExec(t *testing.T) { func TestRolePermissions(t *testing.T) { t.Parallel() + crud := []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} + auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) // currentUser is anything that references "me", "mine", or "my". @@ -145,8 +147,8 @@ func TestRolePermissions(t *testing.T) { { Name: "MyWorkspaceInOrgExecution", // When creating the WithID won't be set, but it does not change the result. - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceWorkspaceExecution.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), + Actions: []policy.Action{policy.ActionSSH}, + Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgMemberMe}, false: {orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, @@ -155,16 +157,16 @@ func TestRolePermissions(t *testing.T) { { Name: "MyWorkspaceInOrgAppConnect", // When creating the WithID won't be set, but it does not change the result. - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceWorkspaceApplicationConnect.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), + Actions: []policy.Action{policy.ActionApplicationConnect}, + Resource: rbac.ResourceWorkspace.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: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin, orgAdmin}, }, }, { Name: "Templates", - Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights}, Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgAdmin, templateAdmin}, @@ -191,7 +193,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "MyFile", - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead}, Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ true: {owner, memberMe, orgMemberMe, templateAdmin}, @@ -227,8 +229,8 @@ func TestRolePermissions(t *testing.T) { }, { Name: "RoleAssignment", - Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceRoleAssignment, + Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete}, + Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]authSubject{ true: {owner, userAdmin}, false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, @@ -237,7 +239,7 @@ func TestRolePermissions(t *testing.T) { { Name: "ReadRoleAssignment", Actions: []policy.Action{policy.ActionRead}, - Resource: rbac.ResourceRoleAssignment, + Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]authSubject{ true: {owner, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, false: {}, @@ -245,8 +247,8 @@ func TestRolePermissions(t *testing.T) { }, { Name: "OrgRoleAssignment", - Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID), + Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete}, + Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgAdmin}, false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, @@ -255,7 +257,7 @@ func TestRolePermissions(t *testing.T) { { Name: "ReadOrgRoleAssignment", Actions: []policy.Action{policy.ActionRead}, - Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID), + Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgAdmin, orgMemberMe}, false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, @@ -264,7 +266,7 @@ func TestRolePermissions(t *testing.T) { { Name: "APIKey", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceAPIKey.WithID(apiKeyID).WithOwner(currentUser.String()), + Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgMemberMe, memberMe}, false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, @@ -272,8 +274,8 @@ func TestRolePermissions(t *testing.T) { }, { Name: "UserData", - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceUserData.WithID(currentUser).WithOwner(currentUser.String()), + Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal}, + Resource: rbac.ResourceUserObject(currentUser), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgMemberMe, memberMe, userAdmin}, false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin}, @@ -312,6 +314,15 @@ func TestRolePermissions(t *testing.T) { }, { Name: "Groups", + Actions: []policy.Action{policy.ActionCreate, policy.ActionDelete, policy.ActionUpdate}, + Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID), + AuthorizeMap: map[bool][]authSubject{ + true: {owner, orgAdmin, userAdmin}, + false: {memberMe, otherOrgAdmin, orgMemberMe, otherOrgMember, templateAdmin}, + }, + }, + { + Name: "GroupsRead", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ @@ -330,19 +341,183 @@ func TestRolePermissions(t *testing.T) { }, { Name: "WorkspaceBuild", - Actions: rbac.AllActions(), - Resource: rbac.ResourceWorkspaceBuild.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), + Actions: []policy.Action{policy.ActionWorkspaceBuild}, + Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgAdmin, orgMemberMe}, false: {userAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, memberMe}, }, }, + // Some admin style resources + { + Name: "Licences", + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, + Resource: rbac.ResourceLicense, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "DeploymentStats", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceDeploymentStats, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "DeploymentConfig", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceDeploymentConfig, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "DebugInfo", + Actions: []policy.Action{policy.ActionUse}, + Resource: rbac.ResourceDebugInfo, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "Replicas", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceReplicas, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "TailnetCoordinator", + Actions: crud, + Resource: rbac.ResourceTailnetCoordinator, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "AuditLogs", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceAuditLog, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "ProvisionerDaemons", + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), + AuthorizeMap: map[bool][]authSubject{ + true: {owner, templateAdmin, orgAdmin}, + false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin}, + }, + }, + { + Name: "ProvisionerDaemonsRead", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), + AuthorizeMap: map[bool][]authSubject{ + // This should be fixed when multi-org goes live + true: {owner, templateAdmin, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin}, + false: {}, + }, + }, + { + Name: "UserProvisionerDaemons", + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), + AuthorizeMap: map[bool][]authSubject{ + true: {owner, templateAdmin, orgMemberMe, orgAdmin}, + false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, + }, + }, + { + Name: "System", + Actions: crud, + Resource: rbac.ResourceSystem, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "Oauth2App", + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourceOauth2App, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "Oauth2AppRead", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceOauth2App, + AuthorizeMap: map[bool][]authSubject{ + true: {owner, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {}, + }, + }, + { + Name: "Oauth2AppSecret", + Actions: crud, + Resource: rbac.ResourceOauth2AppSecret, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "Oauth2Token", + Actions: crud, + Resource: rbac.ResourceOauth2AppCodeToken, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "WorkspaceProxy", + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourceWorkspaceProxy, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "WorkspaceProxyRead", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceWorkspaceProxy, + AuthorizeMap: map[bool][]authSubject{ + true: {owner, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {}, + }, + }, + } + + // We expect every permission to be tested above. + remainingPermissions := make(map[string]map[policy.Action]bool) + for rtype, perms := range policy.RBACPermissions { + remainingPermissions[rtype] = make(map[policy.Action]bool) + for action := range perms.Actions { + remainingPermissions[rtype][action] = true + } } for _, c := range testCases { c := c + // nolint:tparallel -- These share the same remainingPermissions map t.Run(c.Name, func(t *testing.T) { - t.Parallel() remainingSubjs := make(map[string]struct{}) for _, subj := range requiredSubjects { remainingSubjs[subj.Name] = struct{}{} @@ -359,6 +534,8 @@ func TestRolePermissions(t *testing.T) { if actor.Scope == nil { actor.Scope = rbac.ScopeAll } + + delete(remainingPermissions[c.Resource.Type], action) err := auth.Authorize(context.Background(), actor, action, c.Resource) if result { assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg)) @@ -371,6 +548,15 @@ func TestRolePermissions(t *testing.T) { require.Empty(t, remainingSubjs, "test should cover all subjects") }) } + + for rtype, v := range remainingPermissions { + // nolint:tparallel -- Making a subtest for easier diagnosing failures. + t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) { + if len(v) > 0 { + assert.Equal(t, map[policy.Action]bool{}, v, "remaining permissions should be empty for type %q", rtype) + } + }) + } } func TestIsOrgRole(t *testing.T) { From 582fdcd64a774e303d65d9b47d18e460b360f94b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 18:43:14 -0500 Subject: [PATCH 15/24] workspace dormancy --- coderd/rbac/policy/policy.go | 38 +++++++++++++++++-------------- coderd/rbac/roles.go | 9 ++++++-- coderd/rbac/roles_test.go | 44 ++++++++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index b216235698735..0efae69649459 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -82,6 +82,21 @@ func (a ActionDefinition) Requires() string { return strings.Join(fields, ",") } +var workspaceActions = map[Action]ActionDefinition{ + ActionCreate: actDef(fieldOwner|fieldOrg, "create a new workspace"), + ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data to view on the UI"), + // TODO: Make updates more granular + ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "edit workspace settings (scheduling, permissions, parameters)"), + ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete workspace"), + + // Workspace provisioning + ActionWorkspaceBuild: actDef(fieldOwner|fieldOrg|fieldACL, "allows starting, stopping, and updating a workspace"), + + // Running a workspace + ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"), + ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"), +} + // RBACPermissions is indexed by the type var RBACPermissions = map[string]PermissionDefinition{ // Wildcard is every object, and the action "*" provides all actions. @@ -104,23 +119,11 @@ var RBACPermissions = map[string]PermissionDefinition{ }, }, "workspace": { - Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOwner|fieldOrg, "create a new workspace"), - ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data to view on the UI"), - // TODO: Make updates more granular - ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "edit workspace settings (scheduling, permissions, parameters)"), - ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete workspace"), - - // Workspace provisioning - ActionWorkspaceBuild: actDef(fieldOwner|fieldOrg|fieldACL, "allows starting, stopping, and updating a workspace"), - - // Running a workspace - ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"), - ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"), - }, + Actions: workspaceActions, }, + // Dormant workspaces have the same perms as workspaces. "workspace_dormant": { - Actions: map[Action]ActionDefinition{}, + Actions: workspaceActions, }, "workspace_proxy": { Actions: map[Action]ActionDefinition{ @@ -194,8 +197,9 @@ var RBACPermissions = map[string]PermissionDefinition{ "organization": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef(0, "create an organization"), - ActionRead: actDef(0, "read organizations"), - ActionDelete: actDef(0, "delete a organization"), + ActionRead: actDef(fieldOrg, "read organizations"), + ActionUpdate: actDef(fieldOrg, "update an organization"), + ActionDelete: actDef(fieldOrg, "delete an organization"), }, }, "organization_member": { diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 8744eb05a7173..2737c3542ab40 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -145,7 +145,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) { allPermsExcept(ResourceWorkspaceDormant, ResourceWorkspace), // This adds back in the Workspace permissions. Permissions(map[string][]policy.Action{ - ResourceWorkspace.Type: ownerWorkspaceActions, + ResourceWorkspace.Type: ownerWorkspaceActions, + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate}, })...), Org: map[string][]Permission{}, User: []Permission{}, @@ -165,6 +166,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Org: map[string][]Permission{}, User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember), Permissions(map[string][]policy.Action{ + // Reduced permission set on dormant workspaces. No build, ssh, or exec + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate}, + // Users cannot do create/update/delete on themselves, but they // can read their own details. ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, @@ -268,7 +272,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Org: map[string][]Permission{ // Org admins should not have workspace exec perms. organizationID: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant), Permissions(map[string][]policy.Action{ - ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate}, + ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), })...), }, User: []Permission{}, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index ea237aa560dfd..361ee5374ca82 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -265,7 +265,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "APIKey", - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgMemberMe, memberMe}, @@ -332,7 +332,16 @@ func TestRolePermissions(t *testing.T) { }, { Name: "WorkspaceDormant", - Actions: rbac.AllActions(), + Actions: crud, + Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), + AuthorizeMap: map[bool][]authSubject{ + true: {orgMemberMe, orgAdmin, owner}, + false: {userAdmin, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, + }, + }, + { + Name: "WorkspaceDormantUse", + Actions: []policy.Action{policy.ActionWorkspaceBuild, policy.ActionApplicationConnect, policy.ActionSSH}, Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]authSubject{ true: {}, @@ -478,7 +487,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "Oauth2Token", - Actions: crud, + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceOauth2AppCodeToken, AuthorizeMap: map[bool][]authSubject{ true: {owner}, @@ -514,6 +523,7 @@ func TestRolePermissions(t *testing.T) { } } + passed := true for _, c := range testCases { c := c // nolint:tparallel -- These share the same remainingPermissions map @@ -524,6 +534,13 @@ func TestRolePermissions(t *testing.T) { } for _, action := range c.Actions { + err := c.Resource.ValidAction(action) + ok := assert.NoError(t, err, "%q is not a valid action for type %q", action, c.Resource.Type) + if !ok { + passed = passed && assert.NoError(t, err, "%q is not a valid action for type %q", action, c.Resource.Type) + continue + } + for result, subjs := range c.AuthorizeMap { for _, subj := range subjs { delete(remainingSubjs, subj.Name) @@ -538,9 +555,9 @@ func TestRolePermissions(t *testing.T) { delete(remainingPermissions[c.Resource.Type], action) err := auth.Authorize(context.Background(), actor, action, c.Resource) if result { - assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg)) + passed = passed && assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg)) } else { - assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg)) + passed = passed && assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg)) } } } @@ -549,13 +566,16 @@ func TestRolePermissions(t *testing.T) { }) } - for rtype, v := range remainingPermissions { - // nolint:tparallel -- Making a subtest for easier diagnosing failures. - t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) { - if len(v) > 0 { - assert.Equal(t, map[policy.Action]bool{}, v, "remaining permissions should be empty for type %q", rtype) - } - }) + // Only run these if the tests on top passed. Otherwise, the error output is too noisy. + if passed { + for rtype, v := range remainingPermissions { + // nolint:tparallel -- Making a subtest for easier diagnosing failures. + t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) { + if len(v) > 0 { + assert.Equal(t, map[policy.Action]bool{}, v, "remaining permissions should be empty for type %q", rtype) + } + }) + } } } From fc4af1e9c20e6cf097cfcfe6ca7a03a2afb00a08 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 18:46:37 -0500 Subject: [PATCH 16/24] remove rbac object fields from policy def --- coderd/rbac/object_gen.go | 167 +++++++++++++++-------------- coderd/rbac/policy/policy.go | 192 ++++++++++++++-------------------- scripts/rbacgen/object.gotmpl | 2 +- 3 files changed, 166 insertions(+), 195 deletions(-) diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 65baa1a0077f4..99b3f0ec6e193 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -17,218 +17,227 @@ var ( // ResourceApiKey // Valid Actions - // - "ActionCreate" needs [owner] :: create an api key - // - "ActionDelete" needs [owner] :: delete an api key - // - "ActionRead" needs [owner] :: read api key details (secrets are not stored) + // - "ActionCreate" :: create an api key + // - "ActionDelete" :: delete an api key + // - "ActionRead" :: read api key details (secrets are not stored) ResourceApiKey = Object{ Type: "api_key", } // ResourceAssignOrgRole // Valid Actions - // - "ActionAssign" needs [] :: ability to assign org scoped roles - // - "ActionDelete" needs [] :: ability to delete org scoped roles + // - "ActionAssign" :: ability to assign org scoped roles + // - "ActionDelete" :: ability to delete org scoped roles + // - "ActionRead" :: view what roles are assignable ResourceAssignOrgRole = Object{ Type: "assign_org_role", } // ResourceAssignRole // Valid Actions - // - "ActionAssign" needs [] :: ability to assign roles - // - "ActionDelete" needs [] :: ability to delete roles - // - "ActionRead" needs [] :: view what roles are assignable + // - "ActionAssign" :: ability to assign roles + // - "ActionDelete" :: ability to delete roles + // - "ActionRead" :: view what roles are assignable ResourceAssignRole = Object{ Type: "assign_role", } // ResourceAuditLog // Valid Actions - // - "ActionRead" needs [] :: read audit logs + // - "ActionRead" :: read audit logs ResourceAuditLog = Object{ Type: "audit_log", } // ResourceDebugInfo // Valid Actions - // - "ActionUse" needs [] :: access to debug routes + // - "ActionUse" :: access to debug routes ResourceDebugInfo = Object{ Type: "debug_info", } // ResourceDeploymentConfig // Valid Actions - // - "ActionRead" needs [] :: read deployment config + // - "ActionRead" :: read deployment config ResourceDeploymentConfig = Object{ Type: "deployment_config", } // ResourceDeploymentStats // Valid Actions - // - "ActionRead" needs [] :: read deployment stats + // - "ActionRead" :: read deployment stats ResourceDeploymentStats = Object{ Type: "deployment_stats", } // ResourceFile // Valid Actions - // - "ActionCreate" needs [] :: create a file - // - "ActionRead" needs [] :: read files + // - "ActionCreate" :: create a file + // - "ActionRead" :: read files ResourceFile = Object{ Type: "file", } // ResourceGroup // Valid Actions - // - "ActionCreate" needs [org] :: create a group - // - "ActionDelete" needs [org] :: delete a group - // - "ActionRead" needs [org] :: read groups - // - "ActionUpdate" needs [org] :: update a group + // - "ActionCreate" :: create a group + // - "ActionDelete" :: delete a group + // - "ActionRead" :: read groups + // - "ActionUpdate" :: update a group ResourceGroup = Object{ Type: "group", } // ResourceLicense // Valid Actions - // - "ActionCreate" needs [] :: create a license - // - "ActionDelete" needs [] :: delete license - // - "ActionRead" needs [] :: read licenses + // - "ActionCreate" :: create a license + // - "ActionDelete" :: delete license + // - "ActionRead" :: read licenses ResourceLicense = Object{ Type: "license", } // ResourceOauth2App // Valid Actions - // - "ActionCreate" needs [] :: make an OAuth2 app. - // - "ActionDelete" needs [] :: delete an OAuth2 app - // - "ActionRead" needs [] :: read OAuth2 apps - // - "ActionUpdate" needs [] :: update the properties of the OAuth2 app. + // - "ActionCreate" :: make an OAuth2 app. + // - "ActionDelete" :: delete an OAuth2 app + // - "ActionRead" :: read OAuth2 apps + // - "ActionUpdate" :: update the properties of the OAuth2 app. ResourceOauth2App = Object{ Type: "oauth2_app", } // ResourceOauth2AppCodeToken // Valid Actions - // - "ActionCreate" needs [] :: - // - "ActionDelete" needs [] :: - // - "ActionRead" needs [] :: + // - "ActionCreate" :: + // - "ActionDelete" :: + // - "ActionRead" :: ResourceOauth2AppCodeToken = Object{ Type: "oauth2_app_code_token", } // ResourceOauth2AppSecret // Valid Actions - // - "ActionCreate" needs [] :: - // - "ActionDelete" needs [] :: - // - "ActionRead" needs [] :: - // - "ActionUpdate" needs [] :: + // - "ActionCreate" :: + // - "ActionDelete" :: + // - "ActionRead" :: + // - "ActionUpdate" :: ResourceOauth2AppSecret = Object{ Type: "oauth2_app_secret", } // ResourceOrganization // Valid Actions - // - "ActionCreate" needs [] :: create an organization - // - "ActionDelete" needs [] :: delete a organization - // - "ActionRead" needs [] :: read organizations + // - "ActionCreate" :: create an organization + // - "ActionDelete" :: delete an organization + // - "ActionRead" :: read organizations + // - "ActionUpdate" :: update an organization ResourceOrganization = Object{ Type: "organization", } // ResourceOrganizationMember // Valid Actions - // - "ActionCreate" needs [org] :: create an organization member - // - "ActionDelete" needs [org] :: delete member - // - "ActionRead" needs [org] :: read member - // - "ActionUpdate" needs [org] :: update a organization member + // - "ActionCreate" :: create an organization member + // - "ActionDelete" :: delete member + // - "ActionRead" :: read member + // - "ActionUpdate" :: update a organization member ResourceOrganizationMember = Object{ Type: "organization_member", } // ResourceProvisionerDaemon // Valid Actions - // - "ActionCreate" needs [org] :: create a provisioner daemon - // - "ActionDelete" needs [org] :: delete a provisioner daemon - // - "ActionRead" needs [org] :: read provisioner daemon - // - "ActionUpdate" needs [org] :: update a provisioner daemon + // - "ActionCreate" :: create a provisioner daemon + // - "ActionDelete" :: delete a provisioner daemon + // - "ActionRead" :: read provisioner daemon + // - "ActionUpdate" :: update a provisioner daemon ResourceProvisionerDaemon = Object{ Type: "provisioner_daemon", } // ResourceReplicas // Valid Actions - // - "ActionRead" needs [] :: read replicas + // - "ActionRead" :: read replicas ResourceReplicas = Object{ Type: "replicas", } // ResourceSystem // Valid Actions - // - "ActionCreate" needs [] :: create system resources - // - "ActionDelete" needs [] :: delete system resources - // - "ActionRead" needs [] :: view system resources - // - "ActionUpdate" needs [] :: update system resources + // - "ActionCreate" :: create system resources + // - "ActionDelete" :: delete system resources + // - "ActionRead" :: view system resources + // - "ActionUpdate" :: update system resources ResourceSystem = Object{ Type: "system", } // ResourceTailnetCoordinator // Valid Actions - // - "ActionCreate" needs [] :: - // - "ActionDelete" needs [] :: - // - "ActionRead" needs [] :: - // - "ActionUpdate" needs [] :: + // - "ActionCreate" :: + // - "ActionDelete" :: + // - "ActionRead" :: + // - "ActionUpdate" :: ResourceTailnetCoordinator = Object{ Type: "tailnet_coordinator", } // ResourceTemplate // Valid Actions - // - "ActionCreate" needs [org] :: create a template - // - "ActionDelete" needs [org,acl] :: delete a template - // - "ActionRead" needs [org,acl] :: read template - // - "ActionUpdate" needs [org,acl] :: update a template - // - "ActionViewInsights" needs [org,acl] :: view insights + // - "ActionCreate" :: create a template + // - "ActionDelete" :: delete a template + // - "ActionRead" :: read template + // - "ActionUpdate" :: update a template + // - "ActionViewInsights" :: view insights ResourceTemplate = Object{ Type: "template", } // ResourceUser // Valid Actions - // - "ActionCreate" needs [] :: create a new user - // - "ActionDelete" needs [] :: delete an existing user - // - "ActionRead" needs [] :: read user data - // - "ActionReadPersonal" needs [owner] :: read personal user data like password - // - "ActionUpdate" needs [] :: update an existing user - // - "ActionUpdatePersonal" needs [owner] :: update personal data + // - "ActionCreate" :: create a new user + // - "ActionDelete" :: delete an existing user + // - "ActionRead" :: read user data + // - "ActionReadPersonal" :: read personal user data like password + // - "ActionUpdate" :: update an existing user + // - "ActionUpdatePersonal" :: update personal data ResourceUser = Object{ Type: "user", } // ResourceWorkspace // Valid Actions - // - "ActionApplicationConnect" needs [owner,org,acl] :: connect to workspace apps via browser - // - "ActionWorkspaceBuild" needs [owner,org,acl] :: allows starting, stopping, and updating a workspace - // - "ActionCreate" needs [owner,org] :: create a new workspace - // - "ActionDelete" needs [owner,org,acl] :: delete workspace - // - "ActionRead" needs [owner,org,acl] :: read workspace data to view on the UI - // - "ActionSSH" needs [owner,org,acl] :: ssh into a given workspace - // - "ActionUpdate" needs [owner,org,acl] :: edit workspace settings (scheduling, permissions, parameters) + // - "ActionApplicationConnect" :: connect to workspace apps via browser + // - "ActionWorkspaceBuild" :: allows starting, stopping, and updating a workspace + // - "ActionCreate" :: create a new workspace + // - "ActionDelete" :: delete workspace + // - "ActionRead" :: read workspace data to view on the UI + // - "ActionSSH" :: ssh into a given workspace + // - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters) ResourceWorkspace = Object{ Type: "workspace", } // ResourceWorkspaceDormant // Valid Actions + // - "ActionApplicationConnect" :: connect to workspace apps via browser + // - "ActionWorkspaceBuild" :: allows starting, stopping, and updating a workspace + // - "ActionCreate" :: create a new workspace + // - "ActionDelete" :: delete workspace + // - "ActionRead" :: read workspace data to view on the UI + // - "ActionSSH" :: ssh into a given workspace + // - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters) ResourceWorkspaceDormant = Object{ Type: "workspace_dormant", } // ResourceWorkspaceProxy // Valid Actions - // - "ActionCreate" needs [] :: create a workspace proxy - // - "ActionDelete" needs [] :: delete a workspace proxy - // - "ActionRead" needs [] :: read and use a workspace proxy - // - "ActionUpdate" needs [] :: update a workspace proxy + // - "ActionCreate" :: create a workspace proxy + // - "ActionDelete" :: delete a workspace proxy + // - "ActionRead" :: read and use a workspace proxy + // - "ActionUpdate" :: update a workspace proxy ResourceWorkspaceProxy = Object{ Type: "workspace_proxy", } @@ -267,16 +276,16 @@ func AllResources() []Objecter { func AllActions() []policy.Action { return []policy.Action{ policy.ActionCreate, - policy.ActionRead, policy.ActionSSH, policy.ActionApplicationConnect, - policy.ActionViewInsights, - policy.ActionWorkspaceBuild, policy.ActionAssign, + policy.ActionReadPersonal, + policy.ActionUpdatePersonal, + policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionUse, - policy.ActionReadPersonal, - policy.ActionUpdatePersonal, + policy.ActionViewInsights, + policy.ActionWorkspaceBuild, } } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 0efae69649459..de4b25372815e 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -1,11 +1,7 @@ package policy -import "strings" - const WildcardSymbol = "*" -type actionFields uint32 - // Action represents the allowed actions to be done on an object. type Action string @@ -28,18 +24,6 @@ const ( ActionUpdatePersonal Action = "update_personal" ) -const ( - // What fields are expected for a given action. - // fieldID: uuid for the resource - fieldID actionFields = 1 << iota - // fieldOwner: expects an 'Owner' value - fieldOwner - // fieldOrg: expects the resource to be owned by an org - fieldOrg - // fieldACL: expects an ACL list to accompany the object - fieldACL -) - type PermissionDefinition struct { // name is optional. Used to override "Type" for function naming. Name string @@ -52,49 +36,27 @@ type PermissionDefinition struct { type ActionDefinition struct { // Human friendly description to explain the action. Description string - - // These booleans enforce these fields are p - Fields actionFields } -func actDef(fields actionFields, description string) ActionDefinition { +func actDef(description string) ActionDefinition { return ActionDefinition{ Description: description, - Fields: fields, } } -func (a ActionDefinition) Requires() string { - fields := make([]string, 0) - if a.Fields&fieldID != 0 { - fields = append(fields, "uuid") - } - if a.Fields&fieldOwner != 0 { - fields = append(fields, "owner") - } - if a.Fields&fieldOrg != 0 { - fields = append(fields, "org") - } - if a.Fields&fieldACL != 0 { - fields = append(fields, "acl") - } - - return strings.Join(fields, ",") -} - var workspaceActions = map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOwner|fieldOrg, "create a new workspace"), - ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data to view on the UI"), + ActionCreate: actDef("create a new workspace"), + ActionRead: actDef("read workspace data to view on the UI"), // TODO: Make updates more granular - ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "edit workspace settings (scheduling, permissions, parameters)"), - ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete workspace"), + ActionUpdate: actDef("edit workspace settings (scheduling, permissions, parameters)"), + ActionDelete: actDef("delete workspace"), // Workspace provisioning - ActionWorkspaceBuild: actDef(fieldOwner|fieldOrg|fieldACL, "allows starting, stopping, and updating a workspace"), + ActionWorkspaceBuild: actDef("allows starting, stopping, and updating a workspace"), // Running a workspace - ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"), - ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"), + ActionSSH: actDef("ssh into a given workspace"), + ActionApplicationConnect: actDef("connect to workspace apps via browser"), } // RBACPermissions is indexed by the type @@ -108,13 +70,13 @@ var RBACPermissions = map[string]PermissionDefinition{ "user": { Actions: map[Action]ActionDefinition{ // Actions deal with site wide user objects. - ActionRead: actDef(0, "read user data"), - ActionCreate: actDef(0, "create a new user"), - ActionUpdate: actDef(0, "update an existing user"), - ActionDelete: actDef(0, "delete an existing user"), + ActionRead: actDef("read user data"), + ActionCreate: actDef("create a new user"), + ActionUpdate: actDef("update an existing user"), + ActionDelete: actDef("delete an existing user"), - ActionReadPersonal: actDef(fieldOwner, "read personal user data like password"), - ActionUpdatePersonal: actDef(fieldOwner, "update personal data"), + ActionReadPersonal: actDef("read personal user data like password"), + ActionUpdatePersonal: actDef("update personal data"), // ActionReadPublic: actDef(fieldOwner, "read public user data"), }, }, @@ -127,152 +89,152 @@ var RBACPermissions = map[string]PermissionDefinition{ }, "workspace_proxy": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, "create a workspace proxy"), - ActionDelete: actDef(0, "delete a workspace proxy"), - ActionUpdate: actDef(0, "update a workspace proxy"), - ActionRead: actDef(0, "read and use a workspace proxy"), + ActionCreate: actDef("create a workspace proxy"), + ActionDelete: actDef("delete a workspace proxy"), + ActionUpdate: actDef("update a workspace proxy"), + ActionRead: actDef("read and use a workspace proxy"), }, }, "license": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, "create a license"), - ActionRead: actDef(0, "read licenses"), - ActionDelete: actDef(0, "delete license"), + ActionCreate: actDef("create a license"), + ActionRead: actDef("read licenses"), + ActionDelete: actDef("delete license"), // Licenses are immutable, so update makes no sense }, }, "audit_log": { Actions: map[Action]ActionDefinition{ - ActionRead: actDef(0, "read audit logs"), + ActionRead: actDef("read audit logs"), }, }, "deployment_config": { Actions: map[Action]ActionDefinition{ - ActionRead: actDef(0, "read deployment config"), + ActionRead: actDef("read deployment config"), }, }, "deployment_stats": { Actions: map[Action]ActionDefinition{ - ActionRead: actDef(0, "read deployment stats"), + ActionRead: actDef("read deployment stats"), }, }, "replicas": { Actions: map[Action]ActionDefinition{ - ActionRead: actDef(0, "read replicas"), + ActionRead: actDef("read replicas"), }, }, "template": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOrg, "create a template"), + ActionCreate: actDef("create a template"), // TODO: Create a use permission maybe? - ActionRead: actDef(fieldOrg|fieldACL, "read template"), - ActionUpdate: actDef(fieldOrg|fieldACL, "update a template"), - ActionDelete: actDef(fieldOrg|fieldACL, "delete a template"), - ActionViewInsights: actDef(fieldOrg|fieldACL, "view insights"), + ActionRead: actDef("read template"), + ActionUpdate: actDef("update a template"), + ActionDelete: actDef("delete a template"), + ActionViewInsights: actDef("view insights"), }, }, "group": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOrg, "create a group"), - ActionRead: actDef(fieldOrg, "read groups"), - ActionDelete: actDef(fieldOrg, "delete a group"), - ActionUpdate: actDef(fieldOrg, "update a group"), + ActionCreate: actDef("create a group"), + ActionRead: actDef("read groups"), + ActionDelete: actDef("delete a group"), + ActionUpdate: actDef("update a group"), }, }, "file": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, "create a file"), - ActionRead: actDef(0, "read files"), + ActionCreate: actDef("create a file"), + ActionRead: actDef("read files"), }, }, "provisioner_daemon": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOrg, "create a provisioner daemon"), + ActionCreate: actDef("create a provisioner daemon"), // TODO: Move to use? - ActionRead: actDef(fieldOrg, "read provisioner daemon"), - ActionUpdate: actDef(fieldOrg, "update a provisioner daemon"), - ActionDelete: actDef(fieldOrg, "delete a provisioner daemon"), + ActionRead: actDef("read provisioner daemon"), + ActionUpdate: actDef("update a provisioner daemon"), + ActionDelete: actDef("delete a provisioner daemon"), }, }, "organization": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, "create an organization"), - ActionRead: actDef(fieldOrg, "read organizations"), - ActionUpdate: actDef(fieldOrg, "update an organization"), - ActionDelete: actDef(fieldOrg, "delete an organization"), + ActionCreate: actDef("create an organization"), + ActionRead: actDef("read organizations"), + ActionUpdate: actDef("update an organization"), + ActionDelete: actDef("delete an organization"), }, }, "organization_member": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOrg, "create an organization member"), - ActionRead: actDef(fieldOrg, "read member"), - ActionUpdate: actDef(fieldOrg, "update a organization member"), - ActionDelete: actDef(fieldOrg, "delete member"), + ActionCreate: actDef("create an organization member"), + ActionRead: actDef("read member"), + ActionUpdate: actDef("update a organization member"), + ActionDelete: actDef("delete member"), }, }, "debug_info": { Actions: map[Action]ActionDefinition{ - ActionUse: actDef(0, "access to debug routes"), + ActionUse: actDef("access to debug routes"), }, }, "system": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, "create system resources"), - ActionRead: actDef(0, "view system resources"), - ActionUpdate: actDef(0, "update system resources"), - ActionDelete: actDef(0, "delete system resources"), + ActionCreate: actDef("create system resources"), + ActionRead: actDef("view system resources"), + ActionUpdate: actDef("update system resources"), + ActionDelete: actDef("delete system resources"), }, }, "api_key": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(fieldOwner, "create an api key"), - ActionRead: actDef(fieldOwner, "read api key details (secrets are not stored)"), - ActionDelete: actDef(fieldOwner, "delete an api key"), + ActionCreate: actDef("create an api key"), + ActionRead: actDef("read api key details (secrets are not stored)"), + ActionDelete: actDef("delete an api key"), }, }, "tailnet_coordinator": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, ""), - ActionRead: actDef(0, ""), - ActionUpdate: actDef(0, ""), - ActionDelete: actDef(0, ""), + ActionCreate: actDef(""), + ActionRead: actDef(""), + ActionUpdate: actDef(""), + ActionDelete: actDef(""), }, }, "assign_role": { Actions: map[Action]ActionDefinition{ - ActionAssign: actDef(0, "ability to assign roles"), - ActionRead: actDef(0, "view what roles are assignable"), - ActionDelete: actDef(0, "ability to delete roles"), + ActionAssign: actDef("ability to assign roles"), + ActionRead: actDef("view what roles are assignable"), + ActionDelete: actDef("ability to delete roles"), }, }, "assign_org_role": { Actions: map[Action]ActionDefinition{ - ActionAssign: actDef(0, "ability to assign org scoped roles"), - ActionRead: actDef(0, "view what roles are assignable"), - ActionDelete: actDef(0, "ability to delete org scoped roles"), + ActionAssign: actDef("ability to assign org scoped roles"), + ActionRead: actDef("view what roles are assignable"), + ActionDelete: actDef("ability to delete org scoped roles"), }, }, "oauth2_app": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, "make an OAuth2 app."), - ActionRead: actDef(0, "read OAuth2 apps"), - ActionUpdate: actDef(0, "update the properties of the OAuth2 app."), - ActionDelete: actDef(0, "delete an OAuth2 app"), + ActionCreate: actDef("make an OAuth2 app."), + ActionRead: actDef("read OAuth2 apps"), + ActionUpdate: actDef("update the properties of the OAuth2 app."), + ActionDelete: actDef("delete an OAuth2 app"), }, }, "oauth2_app_secret": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, ""), - ActionRead: actDef(0, ""), - ActionUpdate: actDef(0, ""), - ActionDelete: actDef(0, ""), + ActionCreate: actDef(""), + ActionRead: actDef(""), + ActionUpdate: actDef(""), + ActionDelete: actDef(""), }, }, "oauth2_app_code_token": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef(0, ""), - ActionRead: actDef(0, ""), - ActionDelete: actDef(0, ""), + ActionCreate: actDef(""), + ActionRead: actDef(""), + ActionDelete: actDef(""), }, }, } diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/object.gotmpl index b005955e88dba..5235d79397a00 100644 --- a/scripts/rbacgen/object.gotmpl +++ b/scripts/rbacgen/object.gotmpl @@ -14,7 +14,7 @@ var ( // Resource{{ $Name }} // Valid Actions {{- range $action, $value := .Actions }} - // - "{{ actionEnum $action }}" needs [{{ $value.Requires }}] :: {{ $value.Description }} + // - "{{ actionEnum $action }}" :: {{ $value.Description }} {{- end }} Resource{{ $Name }} = Object { Type: "{{ $element.Type }}", From 282bc93dd917d46d2faf0d32aa288d283a1228ad Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 19:01:01 -0500 Subject: [PATCH 17/24] add codersdk autogen --- Makefile | 6 +- coderd/authorize.go | 4 +- coderd/rbac/object_gen.go | 10 +-- codersdk/rbacresources.go | 77 ------------------- codersdk/rbacresources_gen.go | 47 +++++++++++ scripts/rbacgen/codersdk.gotmpl | 16 ++++ scripts/rbacgen/main.go | 63 +++++++++++---- .../{object.gotmpl => rbacobject.gotmpl} | 2 +- 8 files changed, 125 insertions(+), 100 deletions(-) delete mode 100644 codersdk/rbacresources.go create mode 100644 codersdk/rbacresources_gen.go create mode 100644 scripts/rbacgen/codersdk.gotmpl rename scripts/rbacgen/{object.gotmpl => rbacobject.gotmpl} (96%) diff --git a/Makefile b/Makefile index 9a457c619ad49..a12f90db05214 100644 --- a/Makefile +++ b/Makefile @@ -486,6 +486,7 @@ gen: \ $(DB_GEN_FILES) \ site/src/api/typesGenerated.ts \ coderd/rbac/object_gen.go \ + codersdk/rbacresources_gen.go \ docs/admin/prometheus.md \ docs/cli.md \ docs/admin/audit-logs.md \ @@ -611,7 +612,10 @@ examples/examples.gen.json: scripts/examplegen/main.go examples/examples.go $(sh go run ./scripts/examplegen/main.go > examples/examples.gen.json 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 + go run scripts/rbacgen/main.go rbac > coderd/rbac/object_gen.go + +codersdk/rbacresources_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go + go run scripts/rbacgen/main.go codersdk > codersdk/rbacresources_gen.go docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics go run scripts/metricsdocgen/main.go diff --git a/coderd/authorize.go b/coderd/authorize.go index 092200921ac5f..2f16fb8ceb720 100644 --- a/coderd/authorize.go +++ b/coderd/authorize.go @@ -169,7 +169,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.String(), + Type: string(v.Object.ResourceType), } if obj.Owner == "me" { obj.Owner = auth.ID @@ -189,7 +189,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.String() { + switch string(v.Object.ResourceType) { case rbac.ResourceWorkspace.Type: dbObj, dbErr = api.Database.GetWorkspaceByID(ctx, id) case rbac.ResourceTemplate.Type: diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 99b3f0ec6e193..d5190924d14dd 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -275,15 +275,15 @@ func AllResources() []Objecter { func AllActions() []policy.Action { return []policy.Action{ - policy.ActionCreate, - policy.ActionSSH, policy.ActionApplicationConnect, policy.ActionAssign, - policy.ActionReadPersonal, - policy.ActionUpdatePersonal, + policy.ActionCreate, + policy.ActionDelete, policy.ActionRead, + policy.ActionReadPersonal, + policy.ActionSSH, policy.ActionUpdate, - policy.ActionDelete, + policy.ActionUpdatePersonal, policy.ActionUse, policy.ActionViewInsights, policy.ActionWorkspaceBuild, diff --git a/codersdk/rbacresources.go b/codersdk/rbacresources.go deleted file mode 100644 index 4b517e544e28f..0000000000000 --- a/codersdk/rbacresources.go +++ /dev/null @@ -1,77 +0,0 @@ -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" - ResourceUserWorkspaceBuildParameters RBACResource = "user_workspace_build_parameters" - 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" - ResourceTemplateInsights RBACResource = "template_insights" -) - -const ( - ActionCreate = "create" - ActionRead = "read" - ActionUpdate = "update" - ActionDelete = "delete" -) - -var ( - AllRBACResources = []RBACResource{ - ResourceWorkspace, - ResourceWorkspaceProxy, - ResourceWorkspaceExecution, - ResourceWorkspaceApplicationConnect, - ResourceAuditLog, - ResourceTemplate, - ResourceGroup, - ResourceFile, - ResourceProvisionerDaemon, - ResourceOrganization, - ResourceRoleAssignment, - ResourceOrgRoleAssignment, - ResourceAPIKey, - ResourceUser, - ResourceUserData, - ResourceUserWorkspaceBuildParameters, - ResourceOrganizationMember, - ResourceLicense, - ResourceDeploymentValues, - ResourceDeploymentStats, - ResourceReplicas, - ResourceDebugInfo, - ResourceSystem, - ResourceTemplateInsights, - } - - AllRBACActions = []string{ - ActionCreate, - ActionRead, - ActionUpdate, - ActionDelete, - } -) - -func (r RBACResource) String() string { - return string(r) -} diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go new file mode 100644 index 0000000000000..fc924b0a4cd9e --- /dev/null +++ b/codersdk/rbacresources_gen.go @@ -0,0 +1,47 @@ +// Code generated by rbacgen/main.go. DO NOT EDIT. +package codersdk + +type RBACResource string + +const ( + ResourceWildcard RBACResource = "*" + ResourceApiKey RBACResource = "api_key" + ResourceAssignOrgRole RBACResource = "assign_org_role" + ResourceAssignRole RBACResource = "assign_role" + ResourceAuditLog RBACResource = "audit_log" + ResourceDebugInfo RBACResource = "debug_info" + ResourceDeploymentConfig RBACResource = "deployment_config" + ResourceDeploymentStats RBACResource = "deployment_stats" + ResourceFile RBACResource = "file" + ResourceGroup RBACResource = "group" + ResourceLicense RBACResource = "license" + ResourceOauth2App RBACResource = "oauth2_app" + ResourceOauth2AppCodeToken RBACResource = "oauth2_app_code_token" + ResourceOauth2AppSecret RBACResource = "oauth2_app_secret" + ResourceOrganization RBACResource = "organization" + ResourceOrganizationMember RBACResource = "organization_member" + ResourceProvisionerDaemon RBACResource = "provisioner_daemon" + ResourceReplicas RBACResource = "replicas" + ResourceSystem RBACResource = "system" + ResourceTailnetCoordinator RBACResource = "tailnet_coordinator" + ResourceTemplate RBACResource = "template" + ResourceUser RBACResource = "user" + ResourceWorkspace RBACResource = "workspace" + ResourceWorkspaceDormant RBACResource = "workspace_dormant" + ResourceWorkspaceProxy RBACResource = "workspace_proxy" +) + +const ( + ActionApplicationConnect = "application_connect" + ActionAssign = "assign" + ActionCreate = "create" + ActionDelete = "delete" + ActionRead = "read" + ActionReadPersonal = "read_personal" + ActionSSH = "ssh" + ActionUpdate = "update" + ActionUpdatePersonal = "update_personal" + ActionUse = "use" + ActionViewInsights = "view_insights" + ActionWorkspaceBuild = "build" +) diff --git a/scripts/rbacgen/codersdk.gotmpl b/scripts/rbacgen/codersdk.gotmpl new file mode 100644 index 0000000000000..d8328e2160120 --- /dev/null +++ b/scripts/rbacgen/codersdk.gotmpl @@ -0,0 +1,16 @@ +// Code generated by rbacgen/main.go. DO NOT EDIT. +package codersdk + +type RBACResource string + +const ( + {{- range $element := . }} + Resource{{ pascalCaseName $element.FunctionName }} RBACResource = "{{ $element.Type }}" + {{- end }} +) + +const ( + {{- range $element := actionsList }} + {{ $element.Enum }} = "{{ $element.Value }}" + {{- end }} +) diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 9f62fa249feda..5bd88d9767ca6 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -2,9 +2,9 @@ package main import ( "bytes" - "context" _ "embed" "errors" + "flag" "fmt" "go/ast" "go/format" @@ -19,17 +19,41 @@ import ( "github.com/coder/coder/v2/coderd/rbac/policy" ) -//go:embed object.gotmpl -var objectGoTpl string +//go:embed rbacobject.gotmpl +var rbacObjectTemplate string + +//go:embed codersdk.gotmpl +var codersdkTemplate string + +func usage() { + _, _ = fmt.Println("Usage: rbacgen ") + _, _ = fmt.Println("Must choose a template target.") +} // 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() + flag.Parse() + + if len(flag.Args()) < 1 { + usage() + os.Exit(1) + } + + var source string + switch strings.ToLower(flag.Args()[0]) { + case "codersdk": + source = codersdkTemplate + case "rbac": + source = rbacObjectTemplate + default: + _, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf("%q is not a valid templte target", flag.Args()[0])) + usage() + os.Exit(2) + } - out, err := generate(ctx) + out, err := generateRbacObjects(source) if err != nil { log.Fatalf("Generate source: %s", err.Error()) } @@ -108,25 +132,36 @@ fileDeclLoop: return actions } -func generate(ctx context.Context) ([]byte, error) { +type ActionDetails struct { + Enum string + Value string +} + +func generateRbacObjects(templateSource string) ([]byte, error) { // Parse the policy.go file for the action enums f, err := parser.ParseFile(token.NewFileSet(), "./coderd/rbac/policy/policy.go", nil, parser.ParseComments) if err != nil { return nil, fmt.Errorf("parsing policy.go: %w", err) } actionMap := fileActions(f) + actionList := make([]ActionDetails, 0) + for value, enum := range actionMap { + actionList = append(actionList, ActionDetails{ + Enum: enum, + Value: value, + }) + } + slices.SortFunc(actionList, func(a, b ActionDetails) int { + return strings.Compare(a.Enum, b.Enum) + }) var errorList []error var x int tpl, err := template.New("object.gotmpl").Funcs(template.FuncMap{ "capitalize": capitalize, "pascalCaseName": pascalCaseName[string], - "actionsList": func() []string { - tmp := make([]string, 0) - for _, actionEnum := range actionMap { - tmp = append(tmp, actionEnum) - } - return tmp + "actionsList": func() []ActionDetails { + return actionList }, "actionEnum": func(action policy.Action) string { x++ @@ -137,7 +172,7 @@ func generate(ctx context.Context) ([]byte, error) { return v }, "concat": func(strs ...string) string { return strings.Join(strs, "") }, - }).Parse(objectGoTpl) + }).Parse(templateSource) if err != nil { return nil, fmt.Errorf("parse template: %w", err) } diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/rbacobject.gotmpl similarity index 96% rename from scripts/rbacgen/object.gotmpl rename to scripts/rbacgen/rbacobject.gotmpl index 5235d79397a00..9e529d2986817 100644 --- a/scripts/rbacgen/object.gotmpl +++ b/scripts/rbacgen/rbacobject.gotmpl @@ -33,7 +33,7 @@ func AllResources() []Objecter { func AllActions() []policy.Action { return []policy.Action { {{- range $element := actionsList }} - policy.{{ $element }}, + policy.{{ $element.Enum }}, {{- end }} } } From 0fab76f77f9a8e5f7f394bc6039c7dca5f7bf389 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 19:20:06 -0500 Subject: [PATCH 18/24] fix ui enums --- codersdk/rbacresources_gen.go | 26 +++++---- scripts/rbacgen/codersdk.gotmpl | 4 +- site/src/api/typesGenerated.ts | 55 +++++++++++++++---- .../src/pages/TemplatePage/TemplateLayout.tsx | 5 +- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index fc924b0a4cd9e..8741f8c5f22cc 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -31,17 +31,19 @@ const ( ResourceWorkspaceProxy RBACResource = "workspace_proxy" ) +type RBACAction string + const ( - ActionApplicationConnect = "application_connect" - ActionAssign = "assign" - ActionCreate = "create" - ActionDelete = "delete" - ActionRead = "read" - ActionReadPersonal = "read_personal" - ActionSSH = "ssh" - ActionUpdate = "update" - ActionUpdatePersonal = "update_personal" - ActionUse = "use" - ActionViewInsights = "view_insights" - ActionWorkspaceBuild = "build" + ActionApplicationConnect RBACAction = "application_connect" + ActionAssign RBACAction = "assign" + ActionCreate RBACAction = "create" + ActionDelete RBACAction = "delete" + ActionRead RBACAction = "read" + ActionReadPersonal RBACAction = "read_personal" + ActionSSH RBACAction = "ssh" + ActionUpdate RBACAction = "update" + ActionUpdatePersonal RBACAction = "update_personal" + ActionUse RBACAction = "use" + ActionViewInsights RBACAction = "view_insights" + ActionWorkspaceBuild RBACAction = "build" ) diff --git a/scripts/rbacgen/codersdk.gotmpl b/scripts/rbacgen/codersdk.gotmpl index d8328e2160120..1492eaf86c2bf 100644 --- a/scripts/rbacgen/codersdk.gotmpl +++ b/scripts/rbacgen/codersdk.gotmpl @@ -9,8 +9,10 @@ const ( {{- end }} ) +type RBACAction string + const ( {{- range $element := actionsList }} - {{ $element.Enum }} = "{{ $element.Value }}" + {{ $element.Enum }} RBACAction = "{{ $element.Value }}" {{- end }} ) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 8d49bc6ca7223..52d62f2415526 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -134,7 +134,7 @@ export interface AuthMethods { // From codersdk/authorization.go export interface AuthorizationCheck { readonly object: AuthorizationObject; - readonly action: string; + readonly action: RBACAction; } // From codersdk/authorization.go @@ -2055,10 +2055,39 @@ export const ProxyHealthStatuses: ProxyHealthStatus[] = [ "unregistered", ]; -// From codersdk/rbacresources.go +// From codersdk/rbacresources_gen.go +export type RBACAction = + | "application_connect" + | "assign" + | "build" + | "create" + | "delete" + | "read" + | "read_personal" + | "ssh" + | "update" + | "update_personal" + | "use" + | "view_insights"; +export const RBACActions: RBACAction[] = [ + "application_connect", + "assign", + "build", + "create", + "delete", + "read", + "read_personal", + "ssh", + "update", + "update_personal", + "use", + "view_insights", +]; + +// From codersdk/rbacresources_gen.go export type RBACResource = + | "*" | "api_key" - | "application_connect" | "assign_org_role" | "assign_role" | "audit_log" @@ -2068,22 +2097,23 @@ export type RBACResource = | "file" | "group" | "license" + | "oauth2_app" + | "oauth2_app_code_token" + | "oauth2_app_secret" | "organization" | "organization_member" | "provisioner_daemon" | "replicas" | "system" + | "tailnet_coordinator" | "template" - | "template_insights" | "user" - | "user_data" - | "user_workspace_build_parameters" | "workspace" - | "workspace_execution" + | "workspace_dormant" | "workspace_proxy"; export const RBACResources: RBACResource[] = [ + "*", "api_key", - "application_connect", "assign_org_role", "assign_role", "audit_log", @@ -2093,18 +2123,19 @@ export const RBACResources: RBACResource[] = [ "file", "group", "license", + "oauth2_app", + "oauth2_app_code_token", + "oauth2_app_secret", "organization", "organization_member", "provisioner_daemon", "replicas", "system", + "tailnet_coordinator", "template", - "template_insights", "user", - "user_data", - "user_workspace_build_parameters", "workspace", - "workspace_execution", + "workspace_dormant", "workspace_proxy", ]; diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index ec19d80c166cc..bd53a6dc39052 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -28,9 +28,10 @@ const templatePermissions = ( }, canReadInsights: { object: { - resource_type: "template_insights", + resource_type: "template", + resource_id: templateId, }, - action: "read", + action: "view_insights", }, }); From ba4c32718a04e9a7e096b146b99a8722a2798a85 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 19:29:29 -0500 Subject: [PATCH 19/24] cleanup --- coderd/rbac/roles.go | 5 ----- coderd/rbac/roles_test.go | 1 - support/support.go | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 2737c3542ab40..b3a77c4f1ed9e 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -592,11 +592,6 @@ func roleSplit(role string) (name string, orgID string, err error) { return arr[0], "", nil } -func Perm[T Objecter](f func(o T) []policy.Action) []policy.Action { - var t T - return f(t) -} - // Permissions is just a helper function to make building roles that list out resources // and actions a bit easier. func Permissions(perms map[string][]policy.Action) []Permission { diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 361ee5374ca82..c62a5cb7b5d30 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -56,7 +56,6 @@ func TestOwnerExec(t *testing.T) { }) } -// TODO: add the SYSTEM to the MATRIX func TestRolePermissions(t *testing.T) { t.Parallel() diff --git a/support/support.go b/support/support.go index e49f95e38d045..e35a63acc05c7 100644 --- a/support/support.go +++ b/support/support.go @@ -460,7 +460,7 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) { authChecks := map[string]codersdk.AuthorizationCheck{ "Read DeploymentValues": { Object: codersdk.AuthorizationObject{ - ResourceType: codersdk.ResourceDeploymentValues, + ResourceType: codersdk.ResourceDeploymentConfig, }, Action: string(policy.ActionRead), }, From e4d59aece59e4f3358f89cb5ca69c732017c93e6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 19:52:13 -0500 Subject: [PATCH 20/24] cleanup --- coderd/database/dbauthz/dbauthz.go | 17 +++++++------- coderd/database/dbauthz/dbauthz_test.go | 8 +++---- coderd/rbac/authz_internal_test.go | 30 +++++++++++++++---------- coderd/rbac/object.go | 2 -- coderd/rbac/policy/policy.go | 5 ++--- coderd/rbac/roles.go | 2 +- coderd/rbac/roles_test.go | 6 +++-- coderd/rbac/scopes.go | 6 ++--- coderd/workspaceapps/apptest/apptest.go | 17 +++++++------- codersdk/authorization.go | 2 +- enterprise/coderd/authorize_test.go | 2 +- enterprise/coderd/coderd_test.go | 2 +- enterprise/coderd/templates.go | 3 +-- enterprise/tailnet/pgcoord.go | 2 +- scripts/rbacgen/main.go | 15 +++++++++++-- support/support.go | 7 +++--- 16 files changed, 70 insertions(+), 56 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 5a15dc9cd9dbc..13b98940f8a58 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -165,12 +165,12 @@ var ( Site: rbac.Permissions(map[string][]policy.Action{ // TODO: Add ProvisionerJob resource type. rbac.ResourceFile.Type: {policy.ActionRead}, - rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, + rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, // Unsure why provisionerd needs update and read personal rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceBuild}, - rbac.ResourceApiKey.Type: {rbac.WildcardSymbol}, + rbac.ResourceApiKey.Type: {policy.WildcardSymbol}, // When org scoped provisioner credentials are implemented, // this can be reduced to read a specific org. rbac.ResourceOrganization.Type: {policy.ActionRead}, @@ -191,7 +191,7 @@ var ( Name: "autostart", DisplayName: "Autostart Daemon", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, + rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceBuild}, rbac.ResourceUser.Type: {policy.ActionRead}, @@ -212,7 +212,7 @@ var ( Name: "hangdetector", DisplayName: "Hang Detector Daemon", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, + rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceTemplate.Type: {policy.ActionRead}, rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate}, }), @@ -234,10 +234,11 @@ var ( rbac.ResourceWildcard.Type: {policy.ActionRead}, rbac.ResourceApiKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate}, - rbac.ResourceAssignRole.Type: {policy.ActionCreate, policy.ActionDelete}, - rbac.ResourceSystem.Type: {rbac.WildcardSymbol}, + rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(), + rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceOrganizationMember.Type: {policy.ActionCreate}, + rbac.ResourceAssignOrgRole.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionDelete}, rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate}, rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(), rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceBuild, policy.ActionSSH}, @@ -607,7 +608,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r } if len(added) > 0 { - if err := q.authorizeContext(ctx, policy.ActionCreate, roleAssign); err != nil { + if err := q.authorizeContext(ctx, policy.ActionAssign, roleAssign); err != nil { return err } } @@ -1775,7 +1776,7 @@ func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetU return nil, err } - if err := q.authorizeContext(ctx, policy.ActionViewInsights, template.RBACObject()); err != nil { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { return nil, err } } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 6d8dfe82f9c53..24c73c331c95f 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -636,7 +636,7 @@ func (s *MethodTestSuite) TestOrganization() { UserID: u.ID, Roles: []string{rbac.RoleOrgAdmin(o.ID)}, }).Asserts( - rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionCreate, + rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionAssign, rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { @@ -656,7 +656,7 @@ func (s *MethodTestSuite) TestOrganization() { OrgID: o.ID, }).Asserts( mem, policy.ActionRead, - rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionCreate, // org-mem + rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionAssign, // org-mem rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionDelete, // org-admin ).Returns(out) })) @@ -1023,7 +1023,7 @@ func (s *MethodTestSuite) TestUser() { check.Args(database.InsertUserParams{ ID: uuid.New(), LoginType: database.LoginTypePassword, - }).Asserts(rbac.ResourceAssignRole, policy.ActionCreate, rbac.ResourceUser, policy.ActionCreate) + }).Asserts(rbac.ResourceAssignRole, policy.ActionAssign, rbac.ResourceUser, policy.ActionCreate) })) s.Run("InsertUserLink", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) @@ -1158,7 +1158,7 @@ func (s *MethodTestSuite) TestUser() { ID: u.ID, }).Asserts( u, policy.ActionRead, - rbac.ResourceAssignRole, policy.ActionCreate, + rbac.ResourceAssignRole, policy.ActionAssign, rbac.ResourceAssignRole, policy.ActionDelete, ).Returns(o) })) diff --git a/coderd/rbac/authz_internal_test.go b/coderd/rbac/authz_internal_test.go index b727d44a4f5ba..7b53939a3651b 100644 --- a/coderd/rbac/authz_internal_test.go +++ b/coderd/rbac/authz_internal_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/rbac/regosql" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/testutil" ) @@ -310,7 +311,7 @@ func TestAuthorizeDomain(t *testing.T) { }, { resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{ - user.ID: {WildcardSymbol}, + user.ID: {policy.WildcardSymbol}, }), actions: ResourceWorkspace.AvailableActions(), allow: true, @@ -342,7 +343,7 @@ func TestAuthorizeDomain(t *testing.T) { }, { resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{ - allUsersGroup: {WildcardSymbol}, + allUsersGroup: {policy.WildcardSymbol}, }), actions: ResourceWorkspace.AvailableActions(), allow: true, @@ -398,8 +399,8 @@ func TestAuthorizeDomain(t *testing.T) { Site: []Permission{ { Negate: true, - ResourceType: WildcardSymbol, - Action: WildcardSymbol, + ResourceType: policy.WildcardSymbol, + Action: policy.WildcardSymbol, }, }, }}, @@ -439,10 +440,13 @@ func TestAuthorizeDomain(t *testing.T) { }, } + workspaceExceptConnect := slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH) + workspaceConnect := []policy.Action{policy.ActionApplicationConnect, policy.ActionSSH} testAuthorize(t, "OrgAdmin", user, []authTestCase{ // Org + me {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, - {resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceExceptConnect, allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceConnect, allow: false}, {resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true}, @@ -453,7 +457,8 @@ func TestAuthorizeDomain(t *testing.T) { {resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false}, // Other org + other user - {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: workspaceExceptConnect, allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: workspaceConnect, allow: false}, {resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false}, @@ -713,8 +718,8 @@ func TestAuthorizeLevels(t *testing.T) { User: []Permission{ { Negate: true, - ResourceType: WildcardSymbol, - Action: WildcardSymbol, + ResourceType: policy.WildcardSymbol, + Action: policy.WildcardSymbol, }, }, }, @@ -761,7 +766,7 @@ func TestAuthorizeLevels(t *testing.T) { { Negate: true, ResourceType: "random", - Action: WildcardSymbol, + Action: policy.WildcardSymbol, }, }, }, @@ -772,8 +777,8 @@ func TestAuthorizeLevels(t *testing.T) { User: []Permission{ { Negate: true, - ResourceType: WildcardSymbol, - Action: WildcardSymbol, + ResourceType: policy.WildcardSymbol, + Action: policy.WildcardSymbol, }, }, }, @@ -782,7 +787,8 @@ func TestAuthorizeLevels(t *testing.T) { testAuthorize(t, "OrgAllowAll", user, cases(func(c authTestCase) authTestCase { - c.actions = ResourceWorkspace.AvailableActions() + // SSH and app connect are not implied here. + c.actions = slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH) return c }, []authTestCase{ // Org + me diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 2022deee495b3..9b4cdf6a37a29 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -8,8 +8,6 @@ import ( "github.com/coder/coder/v2/coderd/rbac/policy" ) -const WildcardSymbol = "*" - // ResourceUserObject is a helper function to create a user object for authz checks. func ResourceUserObject(userID uuid.UUID) Object { return ResourceUser.WithID(userID).WithOwner(userID.String()) diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index de4b25372815e..61103e0a7128e 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -75,9 +75,8 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionUpdate: actDef("update an existing user"), ActionDelete: actDef("delete an existing user"), - ActionReadPersonal: actDef("read personal user data like password"), + ActionReadPersonal: actDef("read personal user data like user settings and auth links"), ActionUpdatePersonal: actDef("update personal data"), - // ActionReadPublic: actDef(fieldOwner, "read public user data"), }, }, "workspace": { @@ -168,7 +167,7 @@ var RBACPermissions = map[string]PermissionDefinition{ Actions: map[Action]ActionDefinition{ ActionCreate: actDef("create an organization member"), ActionRead: actDef("read member"), - ActionUpdate: actDef("update a organization member"), + ActionUpdate: actDef("update an organization member"), ActionDelete: actDef("delete member"), }, }, diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index b3a77c4f1ed9e..23100c0b6fba7 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -92,7 +92,7 @@ func allPermsExcept(excepts ...Objecter) []Permission { perms = append(perms, Permission{ Negate: false, ResourceType: r.RBACObject().Type, - Action: WildcardSymbol, + Action: policy.WildcardSymbol, }) } return perms diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index c62a5cb7b5d30..067c64459972c 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -56,6 +56,7 @@ func TestOwnerExec(t *testing.T) { }) } +// nolint:tparallel,paralleltest -- subtests share a map, just run sequentially. func TestRolePermissions(t *testing.T) { t.Parallel() @@ -523,9 +524,10 @@ func TestRolePermissions(t *testing.T) { } passed := true + // nolint:tparallel,paralleltest for _, c := range testCases { c := c - // nolint:tparallel -- These share the same remainingPermissions map + // nolint:tparallel,paralleltest -- These share the same remainingPermissions map t.Run(c.Name, func(t *testing.T) { remainingSubjs := make(map[string]struct{}) for _, subj := range requiredSubjects { @@ -568,7 +570,7 @@ func TestRolePermissions(t *testing.T) { // Only run these if the tests on top passed. Otherwise, the error output is too noisy. if passed { for rtype, v := range remainingPermissions { - // nolint:tparallel -- Making a subtest for easier diagnosing failures. + // nolint:tparallel,paralleltest -- Making a subtest for easier diagnosing failures. t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) { if len(v) > 0 { assert.Equal(t, map[policy.Action]bool{}, v, "remaining permissions should be empty for type %q", rtype) diff --git a/coderd/rbac/scopes.go b/coderd/rbac/scopes.go index a0b77756a36fe..3eccd8194f31a 100644 --- a/coderd/rbac/scopes.go +++ b/coderd/rbac/scopes.go @@ -61,12 +61,12 @@ var builtinScopes = map[ScopeName]Scope{ Name: fmt.Sprintf("Scope_%s", ScopeAll), DisplayName: "All operations", Site: Permissions(map[string][]policy.Action{ - ResourceWildcard.Type: {WildcardSymbol}, + ResourceWildcard.Type: {policy.WildcardSymbol}, }), Org: map[string][]Permission{}, User: []Permission{}, }, - AllowIDList: []string{WildcardSymbol}, + AllowIDList: []string{policy.WildcardSymbol}, }, ScopeApplicationConnect: { @@ -79,7 +79,7 @@ var builtinScopes = map[ScopeName]Scope{ Org: map[string][]Permission{}, User: []Permission{}, }, - AllowIDList: []string{WildcardSymbol}, + AllowIDList: []string{policy.WildcardSymbol}, }, } diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index 5ba60fbb58687..851d8ff144eb0 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -541,32 +541,31 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { appTokenAPIClient.HTTPClient.Transport = appDetails.SDKClient.HTTPClient.Transport var ( - canCreateApplicationConnect = "can-create-application_connect" - canReadUserMe = "can-read-user-me" + canApplicationConnect = "can-create-application_connect" + canReadUserMe = "can-read-user-me" ) authRes, err := appTokenAPIClient.AuthCheck(ctx, codersdk.AuthorizationRequest{ Checks: map[string]codersdk.AuthorizationCheck{ - canCreateApplicationConnect: { + canApplicationConnect: { Object: codersdk.AuthorizationObject{ - ResourceType: "application_connect", - OwnerID: "me", + ResourceType: "workspace", + OwnerID: appDetails.FirstUser.UserID.String(), OrganizationID: appDetails.FirstUser.OrganizationID.String(), }, - Action: "create", + Action: codersdk.ActionApplicationConnect, }, canReadUserMe: { Object: codersdk.AuthorizationObject{ ResourceType: "user", - OwnerID: "me", ResourceID: appDetails.FirstUser.UserID.String(), }, - Action: "read", + Action: codersdk.ActionRead, }, }, }) require.NoError(t, err) - require.True(t, authRes[canCreateApplicationConnect]) + require.True(t, authRes[canApplicationConnect]) require.False(t, authRes[canReadUserMe]) // Load the application page with the API key set. diff --git a/codersdk/authorization.go b/codersdk/authorization.go index 4e8a6eed7019f..c3cff7abed149 100644 --- a/codersdk/authorization.go +++ b/codersdk/authorization.go @@ -32,7 +32,7 @@ type AuthorizationCheck struct { // Omitting the 'OrganizationID' could produce the incorrect value, as // workspaces have both `user` and `organization` owners. Object AuthorizationObject `json:"object"` - Action string `json:"action" enums:"create,read,update,delete"` + Action RBACAction `json:"action" enums:"create,read,update,delete"` } // AuthorizationObject can represent a "set" of objects, such as: all workspaces in an organization, all workspaces owned by me, diff --git a/enterprise/coderd/authorize_test.go b/enterprise/coderd/authorize_test.go index 8a1fab590acee..30d890b0beab6 100644 --- a/enterprise/coderd/authorize_test.go +++ b/enterprise/coderd/authorize_test.go @@ -59,7 +59,7 @@ func TestCheckACLPermissions(t *testing.T) { ResourceType: codersdk.ResourceTemplate, ResourceID: template.ID.String(), }, - Action: "write", + Action: codersdk.ActionUpdate, }, } diff --git a/enterprise/coderd/coderd_test.go b/enterprise/coderd/coderd_test.go index e53b714b3fe22..d881a21e49423 100644 --- a/enterprise/coderd/coderd_test.go +++ b/enterprise/coderd/coderd_test.go @@ -500,7 +500,7 @@ func testDBAuthzRole(ctx context.Context) context.Context { Name: "testing", DisplayName: "Unit Tests", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceWildcard.Type: {rbac.WildcardSymbol}, + rbac.ResourceWildcard.Type: {policy.WildcardSymbol}, }), Org: map[string][]rbac.Permission{}, User: []rbac.Permission{}, diff --git a/enterprise/coderd/templates.go b/enterprise/coderd/templates.go index cf67179984b51..6caf882192816 100644 --- a/enterprise/coderd/templates.go +++ b/enterprise/coderd/templates.go @@ -15,7 +15,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/codersdk" ) @@ -320,7 +319,7 @@ func convertToTemplateRole(actions []policy.Action) codersdk.TemplateRole { func convertSDKTemplateRole(role codersdk.TemplateRole) []policy.Action { switch role { case codersdk.TemplateRoleAdmin: - return []policy.Action{rbac.WildcardSymbol} + return []policy.Action{policy.WildcardSymbol} case codersdk.TemplateRoleUse: return []policy.Action{policy.ActionRead} } diff --git a/enterprise/tailnet/pgcoord.go b/enterprise/tailnet/pgcoord.go index 8cb9595492feb..baccfe66a7fd7 100644 --- a/enterprise/tailnet/pgcoord.go +++ b/enterprise/tailnet/pgcoord.go @@ -103,7 +103,7 @@ var pgCoordSubject = rbac.Subject{ Name: "tailnetcoordinator", DisplayName: "Tailnet Coordinator", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceTailnetCoordinator.Type: {rbac.WildcardSymbol}, + rbac.ResourceTailnetCoordinator.Type: {policy.WildcardSymbol}, }), Org: map[string][]rbac.Permission{}, User: []rbac.Permission{}, diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 5bd88d9767ca6..0a6a5893e581b 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -41,6 +41,9 @@ func main() { os.Exit(1) } + // It did not make sense to have 2 different generators that do essentially + // the same thing, but different format for the BE and the sdk. + // So the argument switches the go template to use. var source string switch strings.ToLower(flag.Args()[0]) { case "codersdk": @@ -48,7 +51,7 @@ func main() { case "rbac": source = rbacObjectTemplate default: - _, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf("%q is not a valid templte target", flag.Args()[0])) + _, _ = fmt.Fprintf(os.Stderr, fmt.Sprintf("%q is not a valid templte target\n", flag.Args()[0])) usage() os.Exit(2) } @@ -64,7 +67,6 @@ func main() { } _, _ = fmt.Fprint(os.Stdout, string(formatted)) - return } func pascalCaseName[T ~string](name T) string { @@ -91,6 +93,9 @@ func (p Definition) FunctionName() string { return p.Type } +// fileActions is required because we cannot get the variable name of the enum +// at runtime. So parse the package to get it. This is purely to ensure enum +// names are consistent, which is a bit annoying, but not too bad. func fileActions(file *ast.File) map[string]string { // actions is a map from the enum value -> enum name actions := make(map[string]string) @@ -137,6 +142,8 @@ type ActionDetails struct { Value string } +// generateRbacObjects will take the policy.go file, and send it as input +// to the go templates. Some AST of the Action enum is also included. func generateRbacObjects(templateSource string) ([]byte, error) { // Parse the policy.go file for the action enums f, err := parser.ParseFile(token.NewFileSet(), "./coderd/rbac/policy/policy.go", nil, parser.ParseComments) @@ -151,6 +158,8 @@ func generateRbacObjects(templateSource string) ([]byte, error) { Value: value, }) } + + // Sorting actions for auto gen consistency. slices.SortFunc(actionList, func(a, b ActionDetails) int { return strings.Compare(a.Enum, b.Enum) }) @@ -177,6 +186,7 @@ func generateRbacObjects(templateSource string) ([]byte, error) { return nil, fmt.Errorf("parse template: %w", err) } + // Convert to sorted list for autogen consistency. var out bytes.Buffer list := make([]Definition, 0) for t, v := range policy.RBACPermissions { @@ -186,6 +196,7 @@ func generateRbacObjects(templateSource string) ([]byte, error) { Type: t, }) } + slices.SortFunc(list, func(a, b Definition) int { return strings.Compare(a.Type, b.Type) }) diff --git a/support/support.go b/support/support.go index e35a63acc05c7..cc23cf67f6342 100644 --- a/support/support.go +++ b/support/support.go @@ -15,11 +15,10 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/net/netcheck" - "github.com/coder/coder/v2/coderd/healthcheck/derphealth" - "github.com/coder/coder/v2/coderd/rbac/policy" - "github.com/google/uuid" + "github.com/coder/coder/v2/coderd/healthcheck/derphealth" + "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/v2/codersdk" @@ -462,7 +461,7 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) { Object: codersdk.AuthorizationObject{ ResourceType: codersdk.ResourceDeploymentConfig, }, - Action: string(policy.ActionRead), + Action: codersdk.ActionRead, }, } From 11bf1d5ce3ef92568d67e36cd55db3ed4cdf3072 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 21:27:28 -0500 Subject: [PATCH 21/24] make start/stop workspaces different actions --- coderd/coderdtest/coderdtest.go | 2 +- coderd/database/dbauthz/dbauthz.go | 33 +++--- coderd/database/dbauthz/dbauthz_test.go | 25 ++-- coderd/rbac/authz.go | 17 +++ coderd/rbac/authz_test.go | 6 +- coderd/rbac/input.json | 147 ++++++++++++++++++------ coderd/rbac/object.go | 2 +- coderd/rbac/object_gen.go | 6 +- coderd/rbac/policy/policy.go | 18 ++- coderd/rbac/roles.go | 6 +- coderd/rbac/roles_test.go | 20 ++-- coderd/roles.go | 4 +- support/support.go | 6 +- 13 files changed, 202 insertions(+), 90 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 14a2fb9231561..6153f1a68abcb 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -221,7 +221,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can } if options.Authorizer == nil { - defAuth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + defAuth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) if _, ok := t.(*testing.T); ok { options.Authorizer = &RecordingAuthorizer{ Wrapped: defAuth, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 13b98940f8a58..31280b05c34cb 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -168,9 +168,10 @@ var ( rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, // Unsure why provisionerd needs update and read personal - rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, - rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceBuild}, - rbac.ResourceApiKey.Type: {policy.WildcardSymbol}, + rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, + rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop}, + rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, + rbac.ResourceApiKey.Type: {policy.WildcardSymbol}, // When org scoped provisioner credentials are implemented, // this can be reduced to read a specific org. rbac.ResourceOrganization.Type: {policy.ActionRead}, @@ -191,10 +192,11 @@ var ( Name: "autostart", DisplayName: "Autostart Daemon", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceSystem.Type: {policy.WildcardSymbol}, - rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, - rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceBuild}, - rbac.ResourceUser.Type: {policy.ActionRead}, + rbac.ResourceSystem.Type: {policy.WildcardSymbol}, + rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, + rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop}, + rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, + rbac.ResourceUser.Type: {policy.ActionRead}, }), Org: map[string][]rbac.Permission{}, User: []rbac.Permission{}, @@ -232,7 +234,7 @@ var ( DisplayName: "Coder", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceWildcard.Type: {policy.ActionRead}, - rbac.ResourceApiKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + rbac.ResourceApiKey.Type: rbac.ResourceApiKey.AvailableActions(), rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate}, rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(), rbac.ResourceSystem.Type: {policy.WildcardSymbol}, @@ -241,7 +243,8 @@ var ( rbac.ResourceAssignOrgRole.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionDelete}, rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate}, rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(), - rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceBuild, policy.ActionSSH}, + rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop}, + rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH}, rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, }), Org: map[string][]rbac.Permission{}, @@ -2531,9 +2534,11 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW return xerrors.Errorf("get workspace by id: %w", err) } - var action policy.Action = policy.ActionWorkspaceBuild + var action policy.Action = policy.ActionWorkspaceStart if arg.Transition == database.WorkspaceTransitionDelete { action = policy.ActionDelete + } else if arg.Transition == database.WorkspaceTransitionStop { + action = policy.ActionWorkspaceStop } if err = q.authorizeContext(ctx, action, w); err != nil { @@ -3280,7 +3285,7 @@ func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error { } func (q *querier) UpsertApplicationName(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertApplicationName(ctx, value) @@ -3294,7 +3299,7 @@ func (q *querier) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDef } func (q *querier) UpsertHealthSettings(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertHealthSettings(ctx, value) @@ -3329,14 +3334,14 @@ func (q *querier) UpsertLastUpdateCheck(ctx context.Context, value string) error } func (q *querier) UpsertLogoURL(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertLogoURL(ctx, value) } func (q *querier) UpsertNotificationBanners(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceDeploymentConfig); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertNotificationBanners(ctx, value) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 24c73c331c95f..e8dcb2f8ee5bc 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -526,10 +526,10 @@ func (s *MethodTestSuite) TestLicense() { Asserts(rbac.ResourceLicense, policy.ActionCreate) })) s.Run("UpsertLogoURL", s.Subtest(func(db database.Store, check *expects) { - check.Args("value").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) + check.Args("value").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) s.Run("UpsertNotificationBanners", s.Subtest(func(db database.Store, check *expects) { - check.Args("value").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) + check.Args("value").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) s.Run("GetLicenseByID", s.Subtest(func(db database.Store, check *expects) { l, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{ @@ -1432,7 +1432,18 @@ func (s *MethodTestSuite) TestWorkspace() { WorkspaceID: w.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, - }).Asserts(w, policy.ActionWorkspaceBuild) + }).Asserts(w, policy.ActionWorkspaceStart) + })) + s.Run("Stop/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { + t := dbgen.Template(s.T(), db, database.Template{}) + w := dbgen.Workspace(s.T(), db, database.Workspace{ + TemplateID: t.ID, + }) + check.Args(database.InsertWorkspaceBuildParams{ + WorkspaceID: w.ID, + Transition: database.WorkspaceTransitionStop, + Reason: database.BuildReasonInitiator, + }).Asserts(w, policy.ActionWorkspaceStop) })) s.Run("Start/RequireActiveVersion/VersionMismatch/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { t := dbgen.Template(s.T(), db, database.Template{}) @@ -1454,7 +1465,7 @@ func (s *MethodTestSuite) TestWorkspace() { Reason: database.BuildReasonInitiator, TemplateVersionID: v.ID, }).Asserts( - w, policy.ActionWorkspaceBuild, + w, policy.ActionWorkspaceStart, t, policy.ActionUpdate, ) })) @@ -1482,7 +1493,7 @@ func (s *MethodTestSuite) TestWorkspace() { Reason: database.BuildReasonInitiator, TemplateVersionID: v.ID, }).Asserts( - w, policy.ActionWorkspaceBuild, + w, policy.ActionWorkspaceStart, ) })) s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { @@ -2206,13 +2217,13 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args().Asserts() })) s.Run("UpsertApplicationName", s.Subtest(func(db database.Store, check *expects) { - check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) + check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) s.Run("GetHealthSettings", s.Subtest(func(db database.Store, check *expects) { check.Args().Asserts() })) s.Run("UpsertHealthSettings", s.Subtest(func(db database.Store, check *expects) { - check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionCreate) + check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) s.Run("GetDeploymentWorkspaceAgentStats", s.Subtest(func(db database.Store, check *expects) { check.Args(time.Time{}).Asserts() diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 78341fa1d1ef5..859782d0286b1 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -214,6 +214,10 @@ type RegoAuthorizer struct { authorizeHist *prometheus.HistogramVec prepareHist prometheus.Histogram + + // strict checking also verifies the inputs to the authorizer. Making sure + // the action make sense for the input object. + strict bool } var _ Authorizer = (*RegoAuthorizer)(nil) @@ -235,6 +239,13 @@ func NewCachingAuthorizer(registry prometheus.Registerer) Authorizer { return Cacher(NewAuthorizer(registry)) } +// NewStrictCachingAuthorizer is mainly just for testing. +func NewStrictCachingAuthorizer(registry prometheus.Registerer) Authorizer { + auth := NewAuthorizer(registry) + auth.strict = true + return Cacher(auth) +} + func NewAuthorizer(registry prometheus.Registerer) *RegoAuthorizer { queryOnce.Do(func() { var err error @@ -321,6 +332,12 @@ type authSubject struct { // the object. // If an error is returned, the authorization is denied. func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error { + if a.strict { + if err := object.ValidAction(action); err != nil { + return xerrors.Errorf("strict authz check: %w", err) + } + } + start := time.Now() ctx, span := tracing.StartSpan(ctx, trace.WithTimestamp(start), // Reuse the time.Now for metric and trace diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index 4ac8f20d94506..05940856ec583 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -160,7 +160,7 @@ func BenchmarkRBACAuthorize(b *testing.B) { // There is no caching that occurs because a fresh context is used for each // call. And the context needs 'WithCacheCtx' to work. - authorizer := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) // This benchmarks all the simple cases using just user permissions. Groups // are added as noise, but do not do anything. for _, c := range benchCases { @@ -187,7 +187,7 @@ func BenchmarkRBACAuthorizeGroups(b *testing.B) { uuid.MustParse("0632b012-49e0-4d70-a5b3-f4398f1dcd52"), uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"), ) - authorizer := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) // Same benchmark cases, but this time groups will be used to match. // Some '*' permissions will still match, but using a fake action reduces @@ -239,7 +239,7 @@ func BenchmarkRBACFilter(b *testing.B) { uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"), ) - authorizer := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) for _, c := range benchCases { b.Run("PrepareOnly-"+c.Name, func(b *testing.B) { diff --git a/coderd/rbac/input.json b/coderd/rbac/input.json index 5e464168ac5ac..b2755089c5970 100644 --- a/coderd/rbac/input.json +++ b/coderd/rbac/input.json @@ -1,46 +1,121 @@ { - "action": "never-match-action", - "object": { - "id": "9046b041-58ed-47a3-9c3a-de302577875a", - "owner": "00000000-0000-0000-0000-000000000000", - "org_owner": "bf7b72bd-a2b1-4ef2-962c-1d698e0483f6", - "type": "workspace", - "acl_user_list": { - "f041847d-711b-40da-a89a-ede39f70dc7f": ["create"] - }, - "acl_group_list": {} + "action":"delete", + "object":{ + "id":"bf9bba2a-dd6b-4a07-8500-1dac6ddc5171", + "owner":"36e3c780-2ae3-443b-ad3c-ceb410a55d12", + "org_owner":"19285bb2-022d-41c6-a7f2-8ace10825691", + "type":"workspace_dormant", + "acl_user_list":null, + "acl_group_list":null }, - "subject": { - "id": "10d03e62-7703-4df5-a358-4f76577d4e2f", - "roles": [ + "subject":{ + "FriendlyName":"Provisioner Daemon", + "ID":"00000000-0000-0000-0000-000000000000", + "Roles":[ { - "name": "owner", - "display_name": "Owner", - "site": [ + "name":"provisionerd", + "display_name":"Provisioner Daemon", + "site":[ { - "negate": false, - "resource_type": "*", - "action": "*" + "negate":false, + "resource_type":"api_key", + "action":"*" + }, + { + "negate":false, + "resource_type":"file", + "action":"read" + }, + { + "negate":false, + "resource_type":"group", + "action":"read" + }, + { + "negate":false, + "resource_type":"organization", + "action":"read" + }, + { + "negate":false, + "resource_type":"system", + "action":"*" + }, + { + "negate":false, + "resource_type":"template", + "action":"read" + }, + { + "negate":false, + "resource_type":"template", + "action":"update" + }, + { + "negate":false, + "resource_type":"user", + "action":"read_personal" + }, + { + "negate":false, + "resource_type":"user", + "action":"update_personal" + }, + { + "negate":false, + "resource_type":"user", + "action":"read" + }, + { + "negate":false, + "resource_type":"workspace", + "action":"read" + }, + { + "negate":false, + "resource_type":"workspace", + "action":"stop" + }, + { + "negate":false, + "resource_type":"workspace", + "action":"start" + }, + { + "negate":false, + "resource_type":"workspace", + "action":"update" + }, + { + "negate":false, + "resource_type":"workspace", + "action":"delete" + }, + { + "negate":false, + "resource_type":"workspace_dormant", + "action":"read" + }, + { + "negate":false, + "resource_type":"workspace_dormant", + "action":"update" + }, + { + "negate":false, + "resource_type":"workspace_dormant", + "action":"stop" } ], - "org": {}, - "user": [] + "org":{ + + }, + "user":[ + + ] } ], - "groups": ["b617a647-b5d0-4cbe-9e40-26f89710bf18"], - "scope": { - "name": "Scope_all", - "display_name": "All operations", - "site": [ - { - "negate": false, - "resource_type": "*", - "action": "*" - } - ], - "org": {}, - "user": [], - "allow_list": ["*"] - } + "Groups":null, + "Scope":"all" } } diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 9b4cdf6a37a29..30a74e4f825dd 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -39,7 +39,7 @@ func (z Object) ValidAction(action policy.Action) error { return fmt.Errorf("invalid type %q", z.Type) } if _, ok := perms.Actions[action]; !ok { - return fmt.Errorf("invalid action %q", action) + return fmt.Errorf("invalid action %q for type %q", action, z.Type) } return nil diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index d5190924d14dd..e2befe8b77b12 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -209,7 +209,7 @@ var ( // ResourceWorkspace // Valid Actions // - "ActionApplicationConnect" :: connect to workspace apps via browser - // - "ActionWorkspaceBuild" :: allows starting, stopping, and updating a workspace + // - "ActionWorkspaceStart" :: allows starting, stopping, and updating a workspace // - "ActionCreate" :: create a new workspace // - "ActionDelete" :: delete workspace // - "ActionRead" :: read workspace data to view on the UI @@ -222,7 +222,7 @@ var ( // ResourceWorkspaceDormant // Valid Actions // - "ActionApplicationConnect" :: connect to workspace apps via browser - // - "ActionWorkspaceBuild" :: allows starting, stopping, and updating a workspace + // - "ActionWorkspaceStart" :: allows starting, stopping, and updating a workspace // - "ActionCreate" :: create a new workspace // - "ActionDelete" :: delete workspace // - "ActionRead" :: read workspace data to view on the UI @@ -286,6 +286,6 @@ func AllActions() []policy.Action { policy.ActionUpdatePersonal, policy.ActionUse, policy.ActionViewInsights, - policy.ActionWorkspaceBuild, + policy.ActionWorkspaceStart, } } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 61103e0a7128e..26afb0e011ca7 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -16,7 +16,8 @@ const ( ActionApplicationConnect Action = "application_connect" ActionViewInsights Action = "view_insights" - ActionWorkspaceBuild Action = "build" + ActionWorkspaceStart Action = "start" + ActionWorkspaceStop Action = "stop" ActionAssign Action = "assign" @@ -51,8 +52,10 @@ var workspaceActions = map[Action]ActionDefinition{ ActionUpdate: actDef("edit workspace settings (scheduling, permissions, parameters)"), ActionDelete: actDef("delete workspace"), - // Workspace provisioning - ActionWorkspaceBuild: actDef("allows starting, stopping, and updating a workspace"), + // Workspace provisioning. Start & stop are different so dormant workspaces can be + // stopped, but not stared. + ActionWorkspaceStart: actDef("allows starting a workspace"), + ActionWorkspaceStop: actDef("allows stopping a workspace"), // Running a workspace ActionSSH: actDef("ssh into a given workspace"), @@ -104,12 +107,14 @@ var RBACPermissions = map[string]PermissionDefinition{ }, "audit_log": { Actions: map[Action]ActionDefinition{ - ActionRead: actDef("read audit logs"), + ActionRead: actDef("read audit logs"), + ActionCreate: actDef("create new audit log entries"), }, }, "deployment_config": { Actions: map[Action]ActionDefinition{ - ActionRead: actDef("read deployment config"), + ActionRead: actDef("read deployment config"), + ActionUpdate: actDef("updating health information"), }, }, "deployment_stats": { @@ -173,7 +178,7 @@ var RBACPermissions = map[string]PermissionDefinition{ }, "debug_info": { Actions: map[Action]ActionDefinition{ - ActionUse: actDef("access to debug routes"), + ActionRead: actDef("access to debug routes"), }, }, "system": { @@ -189,6 +194,7 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionCreate: actDef("create an api key"), ActionRead: actDef("read api key details (secrets are not stored)"), ActionDelete: actDef("delete an api key"), + ActionUpdate: actDef("update an api key, eg expires"), }, }, "tailnet_coordinator": { diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 23100c0b6fba7..cee365d06624c 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -146,7 +146,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // This adds back in the Workspace permissions. Permissions(map[string][]policy.Action{ ResourceWorkspace.Type: ownerWorkspaceActions, - ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate}, + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop}, })...), Org: map[string][]Permission{}, User: []Permission{}, @@ -167,7 +167,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember), Permissions(map[string][]policy.Action{ // Reduced permission set on dormant workspaces. No build, ssh, or exec - ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate}, + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop}, // Users cannot do create/update/delete on themselves, but they // can read their own details. @@ -272,7 +272,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Org: map[string][]Permission{ // Org admins should not have workspace exec perms. organizationID: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant), Permissions(map[string][]policy.Action{ - ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate}, + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop}, ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), })...), }, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 067c64459972c..44ef83b74cd20 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -34,7 +34,7 @@ func TestOwnerExec(t *testing.T) { }) t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) }) - auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) // Exec a random workspace err := auth.Authorize(context.Background(), owner, policy.ActionSSH, rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString())) @@ -47,7 +47,7 @@ func TestOwnerExec(t *testing.T) { }) t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) }) - auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) // Exec a random workspace err := auth.Authorize(context.Background(), owner, policy.ActionSSH, @@ -62,7 +62,7 @@ func TestRolePermissions(t *testing.T) { crud := []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} - auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) // currentUser is anything that references "me", "mine", or "my". currentUser := uuid.New() @@ -265,7 +265,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "APIKey", - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete, policy.ActionUpdate}, Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgMemberMe, memberMe}, @@ -332,7 +332,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "WorkspaceDormant", - Actions: crud, + Actions: append(crud, policy.ActionWorkspaceStop), Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]authSubject{ true: {orgMemberMe, orgAdmin, owner}, @@ -341,7 +341,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "WorkspaceDormantUse", - Actions: []policy.Action{policy.ActionWorkspaceBuild, policy.ActionApplicationConnect, policy.ActionSSH}, + Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionApplicationConnect, policy.ActionSSH}, Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]authSubject{ true: {}, @@ -350,7 +350,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "WorkspaceBuild", - Actions: []policy.Action{policy.ActionWorkspaceBuild}, + Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]authSubject{ true: {owner, orgAdmin, orgMemberMe}, @@ -378,7 +378,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "DeploymentConfig", - Actions: []policy.Action{policy.ActionRead}, + Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate}, Resource: rbac.ResourceDeploymentConfig, AuthorizeMap: map[bool][]authSubject{ true: {owner}, @@ -387,7 +387,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "DebugInfo", - Actions: []policy.Action{policy.ActionUse}, + Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceDebugInfo, AuthorizeMap: map[bool][]authSubject{ true: {owner}, @@ -414,7 +414,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "AuditLogs", - Actions: []policy.Action{policy.ActionRead}, + Actions: []policy.Action{policy.ActionRead, policy.ActionCreate}, Resource: rbac.ResourceAuditLog, AuthorizeMap: map[bool][]authSubject{ true: {owner}, diff --git a/coderd/roles.go b/coderd/roles.go index ac74dd1c91311..5665e298f0e5d 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -23,7 +23,7 @@ import ( func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() actorRoles := httpmw.UserAuthorization(r) - if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig) { + if !api.Authorize(r, policy.ActionRead, rbac.ResourceAssignRole) { httpapi.Forbidden(rw) return } @@ -47,7 +47,7 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) actorRoles := httpmw.UserAuthorization(r) - if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig.InOrg(organization.ID)) { + if !api.Authorize(r, policy.ActionRead, rbac.ResourceAssignOrgRole.InOrg(organization.ID)) { httpapi.ResourceNotFound(rw) return } diff --git a/support/support.go b/support/support.go index cc23cf67f6342..af3ad21200d02 100644 --- a/support/support.go +++ b/support/support.go @@ -10,17 +10,15 @@ import ( "net/http/httptest" "strings" + "github.com/google/uuid" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "tailscale.com/ipn/ipnstate" "tailscale.com/net/netcheck" - "github.com/google/uuid" - - "github.com/coder/coder/v2/coderd/healthcheck/derphealth" - "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "github.com/coder/coder/v2/coderd/healthcheck/derphealth" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/healthsdk" From a5c2fd5dcd4a9cb91c76d75d1d7af0ced31df5c8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 22:17:43 -0500 Subject: [PATCH 22/24] make fmt --- coderd/rbac/input.json | 150 ++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 77 deletions(-) diff --git a/coderd/rbac/input.json b/coderd/rbac/input.json index b2755089c5970..675c91d15ada4 100644 --- a/coderd/rbac/input.json +++ b/coderd/rbac/input.json @@ -1,121 +1,117 @@ { - "action":"delete", - "object":{ - "id":"bf9bba2a-dd6b-4a07-8500-1dac6ddc5171", - "owner":"36e3c780-2ae3-443b-ad3c-ceb410a55d12", - "org_owner":"19285bb2-022d-41c6-a7f2-8ace10825691", - "type":"workspace_dormant", - "acl_user_list":null, - "acl_group_list":null + "action": "delete", + "object": { + "id": "bf9bba2a-dd6b-4a07-8500-1dac6ddc5171", + "owner": "36e3c780-2ae3-443b-ad3c-ceb410a55d12", + "org_owner": "19285bb2-022d-41c6-a7f2-8ace10825691", + "type": "workspace_dormant", + "acl_user_list": null, + "acl_group_list": null }, - "subject":{ - "FriendlyName":"Provisioner Daemon", - "ID":"00000000-0000-0000-0000-000000000000", - "Roles":[ + "subject": { + "FriendlyName": "Provisioner Daemon", + "ID": "00000000-0000-0000-0000-000000000000", + "Roles": [ { - "name":"provisionerd", - "display_name":"Provisioner Daemon", - "site":[ + "name": "provisionerd", + "display_name": "Provisioner Daemon", + "site": [ { - "negate":false, - "resource_type":"api_key", - "action":"*" + "negate": false, + "resource_type": "api_key", + "action": "*" }, { - "negate":false, - "resource_type":"file", - "action":"read" + "negate": false, + "resource_type": "file", + "action": "read" }, { - "negate":false, - "resource_type":"group", - "action":"read" + "negate": false, + "resource_type": "group", + "action": "read" }, { - "negate":false, - "resource_type":"organization", - "action":"read" + "negate": false, + "resource_type": "organization", + "action": "read" }, { - "negate":false, - "resource_type":"system", - "action":"*" + "negate": false, + "resource_type": "system", + "action": "*" }, { - "negate":false, - "resource_type":"template", - "action":"read" + "negate": false, + "resource_type": "template", + "action": "read" }, { - "negate":false, - "resource_type":"template", - "action":"update" + "negate": false, + "resource_type": "template", + "action": "update" }, { - "negate":false, - "resource_type":"user", - "action":"read_personal" + "negate": false, + "resource_type": "user", + "action": "read_personal" }, { - "negate":false, - "resource_type":"user", - "action":"update_personal" + "negate": false, + "resource_type": "user", + "action": "update_personal" }, { - "negate":false, - "resource_type":"user", - "action":"read" + "negate": false, + "resource_type": "user", + "action": "read" }, { - "negate":false, - "resource_type":"workspace", - "action":"read" + "negate": false, + "resource_type": "workspace", + "action": "read" }, { - "negate":false, - "resource_type":"workspace", - "action":"stop" + "negate": false, + "resource_type": "workspace", + "action": "stop" }, { - "negate":false, - "resource_type":"workspace", - "action":"start" + "negate": false, + "resource_type": "workspace", + "action": "start" }, { - "negate":false, - "resource_type":"workspace", - "action":"update" + "negate": false, + "resource_type": "workspace", + "action": "update" }, { - "negate":false, - "resource_type":"workspace", - "action":"delete" + "negate": false, + "resource_type": "workspace", + "action": "delete" }, { - "negate":false, - "resource_type":"workspace_dormant", - "action":"read" + "negate": false, + "resource_type": "workspace_dormant", + "action": "read" }, { - "negate":false, - "resource_type":"workspace_dormant", - "action":"update" + "negate": false, + "resource_type": "workspace_dormant", + "action": "update" }, { - "negate":false, - "resource_type":"workspace_dormant", - "action":"stop" + "negate": false, + "resource_type": "workspace_dormant", + "action": "stop" } ], - "org":{ - - }, - "user":[ - - ] + "org": {}, + "user": [] } ], - "Groups":null, - "Scope":"all" + "Groups": null, + "Scope": "all" } } From 6be67fc63e68f0ce417813b75b259dee40ca7a70 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 22:18:11 -0500 Subject: [PATCH 23/24] reset input.json --- coderd/rbac/input.json | 129 +++++++++-------------------------------- 1 file changed, 29 insertions(+), 100 deletions(-) diff --git a/coderd/rbac/input.json b/coderd/rbac/input.json index 675c91d15ada4..5e464168ac5ac 100644 --- a/coderd/rbac/input.json +++ b/coderd/rbac/input.json @@ -1,117 +1,46 @@ { - "action": "delete", + "action": "never-match-action", "object": { - "id": "bf9bba2a-dd6b-4a07-8500-1dac6ddc5171", - "owner": "36e3c780-2ae3-443b-ad3c-ceb410a55d12", - "org_owner": "19285bb2-022d-41c6-a7f2-8ace10825691", - "type": "workspace_dormant", - "acl_user_list": null, - "acl_group_list": null + "id": "9046b041-58ed-47a3-9c3a-de302577875a", + "owner": "00000000-0000-0000-0000-000000000000", + "org_owner": "bf7b72bd-a2b1-4ef2-962c-1d698e0483f6", + "type": "workspace", + "acl_user_list": { + "f041847d-711b-40da-a89a-ede39f70dc7f": ["create"] + }, + "acl_group_list": {} }, "subject": { - "FriendlyName": "Provisioner Daemon", - "ID": "00000000-0000-0000-0000-000000000000", - "Roles": [ + "id": "10d03e62-7703-4df5-a358-4f76577d4e2f", + "roles": [ { - "name": "provisionerd", - "display_name": "Provisioner Daemon", + "name": "owner", + "display_name": "Owner", "site": [ { "negate": false, - "resource_type": "api_key", + "resource_type": "*", "action": "*" - }, - { - "negate": false, - "resource_type": "file", - "action": "read" - }, - { - "negate": false, - "resource_type": "group", - "action": "read" - }, - { - "negate": false, - "resource_type": "organization", - "action": "read" - }, - { - "negate": false, - "resource_type": "system", - "action": "*" - }, - { - "negate": false, - "resource_type": "template", - "action": "read" - }, - { - "negate": false, - "resource_type": "template", - "action": "update" - }, - { - "negate": false, - "resource_type": "user", - "action": "read_personal" - }, - { - "negate": false, - "resource_type": "user", - "action": "update_personal" - }, - { - "negate": false, - "resource_type": "user", - "action": "read" - }, - { - "negate": false, - "resource_type": "workspace", - "action": "read" - }, - { - "negate": false, - "resource_type": "workspace", - "action": "stop" - }, - { - "negate": false, - "resource_type": "workspace", - "action": "start" - }, - { - "negate": false, - "resource_type": "workspace", - "action": "update" - }, - { - "negate": false, - "resource_type": "workspace", - "action": "delete" - }, - { - "negate": false, - "resource_type": "workspace_dormant", - "action": "read" - }, - { - "negate": false, - "resource_type": "workspace_dormant", - "action": "update" - }, - { - "negate": false, - "resource_type": "workspace_dormant", - "action": "stop" } ], "org": {}, "user": [] } ], - "Groups": null, - "Scope": "all" + "groups": ["b617a647-b5d0-4cbe-9e40-26f89710bf18"], + "scope": { + "name": "Scope_all", + "display_name": "All operations", + "site": [ + { + "negate": false, + "resource_type": "*", + "action": "*" + } + ], + "org": {}, + "user": [], + "allow_list": ["*"] + } } } From 7262bd807761b5c54359d97df4820cea01deb8ca Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 May 2024 22:26:51 -0500 Subject: [PATCH 24/24] make gen --- coderd/apidoc/docs.go | 113 ++++++++++++++++++---------- coderd/apidoc/swagger.json | 115 +++++++++++++++++++---------- coderd/database/dbauthz/dbauthz.go | 46 ++++++------ coderd/rbac/object_gen.go | 16 ++-- codersdk/rbacresources_gen.go | 3 +- docs/api/authorization.md | 4 +- docs/api/schemas.md | 91 +++++++++++++++-------- scripts/rbacgen/main.go | 2 +- site/src/api/typesGenerated.ts | 6 +- 9 files changed, 255 insertions(+), 141 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 22961a36df98a..0a22d84d13642 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8468,12 +8468,16 @@ const docTemplate = `{ "type": "object", "properties": { "action": { - "type": "string", "enum": [ "create", "read", "update", "delete" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.RBACAction" + } ] }, "object": { @@ -10776,59 +10780,94 @@ const docTemplate = `{ } } }, - "codersdk.RBACResource": { + "codersdk.RBACAction": { "type": "string", "enum": [ - "workspace", - "workspace_proxy", - "workspace_execution", "application_connect", + "assign", + "create", + "delete", + "read", + "read_personal", + "ssh", + "update", + "update_personal", + "use", + "view_insights", + "start", + "stop" + ], + "x-enum-varnames": [ + "ActionApplicationConnect", + "ActionAssign", + "ActionCreate", + "ActionDelete", + "ActionRead", + "ActionReadPersonal", + "ActionSSH", + "ActionUpdate", + "ActionUpdatePersonal", + "ActionUse", + "ActionViewInsights", + "ActionWorkspaceStart", + "ActionWorkspaceStop" + ] + }, + "codersdk.RBACResource": { + "type": "string", + "enum": [ + "*", + "api_key", + "assign_org_role", + "assign_role", "audit_log", - "template", - "group", + "debug_info", + "deployment_config", + "deployment_stats", "file", - "provisioner_daemon", + "group", + "license", + "oauth2_app", + "oauth2_app_code_token", + "oauth2_app_secret", "organization", - "assign_role", - "assign_org_role", - "api_key", - "user", - "user_data", - "user_workspace_build_parameters", "organization_member", - "license", - "deployment_config", - "deployment_stats", + "provisioner_daemon", "replicas", - "debug_info", "system", - "template_insights" + "tailnet_coordinator", + "template", + "user", + "workspace", + "workspace_dormant", + "workspace_proxy" ], "x-enum-varnames": [ - "ResourceWorkspace", - "ResourceWorkspaceProxy", - "ResourceWorkspaceExecution", - "ResourceWorkspaceApplicationConnect", + "ResourceWildcard", + "ResourceApiKey", + "ResourceAssignOrgRole", + "ResourceAssignRole", "ResourceAuditLog", - "ResourceTemplate", - "ResourceGroup", + "ResourceDebugInfo", + "ResourceDeploymentConfig", + "ResourceDeploymentStats", "ResourceFile", - "ResourceProvisionerDaemon", + "ResourceGroup", + "ResourceLicense", + "ResourceOauth2App", + "ResourceOauth2AppCodeToken", + "ResourceOauth2AppSecret", "ResourceOrganization", - "ResourceRoleAssignment", - "ResourceOrgRoleAssignment", - "ResourceAPIKey", - "ResourceUser", - "ResourceUserData", - "ResourceUserWorkspaceBuildParameters", "ResourceOrganizationMember", - "ResourceLicense", - "ResourceDeploymentValues", - "ResourceDeploymentStats", + "ResourceProvisionerDaemon", "ResourceReplicas", - "ResourceDebugInfo", "ResourceSystem", - "ResourceTemplateInsights" + "ResourceTailnetCoordinator", + "ResourceTemplate", + "ResourceUser", + "ResourceWorkspace", + "ResourceWorkspaceDormant", + "ResourceWorkspaceProxy" ] }, "codersdk.RateLimitConfig": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 76b606e46bb8f..331b1512393f7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7537,8 +7537,12 @@ "type": "object", "properties": { "action": { - "type": "string", - "enum": ["create", "read", "update", "delete"] + "enum": ["create", "read", "update", "delete"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.RBACAction" + } + ] }, "object": { "description": "Object can represent a \"set\" of objects, such as: all workspaces in an organization, all workspaces owned by me, and all workspaces across the entire product.\nWhen defining an object, use the most specific language when possible to\nproduce the smallest set. Meaning to set as many fields on 'Object' as\nyou can. Example, if you want to check if you can update all workspaces\nowned by 'me', try to also add an 'OrganizationID' to the settings.\nOmitting the 'OrganizationID' could produce the incorrect value, as\nworkspaces have both `user` and `organization` owners.", @@ -9686,59 +9690,94 @@ } } }, - "codersdk.RBACResource": { + "codersdk.RBACAction": { "type": "string", "enum": [ - "workspace", - "workspace_proxy", - "workspace_execution", "application_connect", + "assign", + "create", + "delete", + "read", + "read_personal", + "ssh", + "update", + "update_personal", + "use", + "view_insights", + "start", + "stop" + ], + "x-enum-varnames": [ + "ActionApplicationConnect", + "ActionAssign", + "ActionCreate", + "ActionDelete", + "ActionRead", + "ActionReadPersonal", + "ActionSSH", + "ActionUpdate", + "ActionUpdatePersonal", + "ActionUse", + "ActionViewInsights", + "ActionWorkspaceStart", + "ActionWorkspaceStop" + ] + }, + "codersdk.RBACResource": { + "type": "string", + "enum": [ + "*", + "api_key", + "assign_org_role", + "assign_role", "audit_log", - "template", - "group", + "debug_info", + "deployment_config", + "deployment_stats", "file", - "provisioner_daemon", + "group", + "license", + "oauth2_app", + "oauth2_app_code_token", + "oauth2_app_secret", "organization", - "assign_role", - "assign_org_role", - "api_key", - "user", - "user_data", - "user_workspace_build_parameters", "organization_member", - "license", - "deployment_config", - "deployment_stats", + "provisioner_daemon", "replicas", - "debug_info", "system", - "template_insights" + "tailnet_coordinator", + "template", + "user", + "workspace", + "workspace_dormant", + "workspace_proxy" ], "x-enum-varnames": [ - "ResourceWorkspace", - "ResourceWorkspaceProxy", - "ResourceWorkspaceExecution", - "ResourceWorkspaceApplicationConnect", + "ResourceWildcard", + "ResourceApiKey", + "ResourceAssignOrgRole", + "ResourceAssignRole", "ResourceAuditLog", - "ResourceTemplate", - "ResourceGroup", + "ResourceDebugInfo", + "ResourceDeploymentConfig", + "ResourceDeploymentStats", "ResourceFile", - "ResourceProvisionerDaemon", + "ResourceGroup", + "ResourceLicense", + "ResourceOauth2App", + "ResourceOauth2AppCodeToken", + "ResourceOauth2AppSecret", "ResourceOrganization", - "ResourceRoleAssignment", - "ResourceOrgRoleAssignment", - "ResourceAPIKey", - "ResourceUser", - "ResourceUserData", - "ResourceUserWorkspaceBuildParameters", "ResourceOrganizationMember", - "ResourceLicense", - "ResourceDeploymentValues", - "ResourceDeploymentStats", + "ResourceProvisionerDaemon", "ResourceReplicas", - "ResourceDebugInfo", "ResourceSystem", - "ResourceTemplateInsights" + "ResourceTailnetCoordinator", + "ResourceTemplate", + "ResourceUser", + "ResourceWorkspace", + "ResourceWorkspaceDormant", + "ResourceWorkspaceProxy" ] }, "codersdk.RateLimitConfig": { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 31280b05c34cb..a096346f57064 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -681,6 +681,29 @@ func authorizedTemplateVersionFromJob(ctx context.Context, q *querier, job datab } } +func (q *querier) authorizeTemplateInsights(ctx context.Context, templateIDs []uuid.UUID) error { + // Abort early if can read all template insights, aka admins. + // TODO: If we know the org, that would allow org admins to abort early too. + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { + for _, templateID := range templateIDs { + template, err := q.db.GetTemplateByID(ctx, templateID) + if err != nil { + return err + } + + if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { + return err + } + } + if len(templateIDs) == 0 { + if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { + return err + } + } + } + return nil +} + func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } @@ -1560,29 +1583,6 @@ func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateD return q.db.GetTemplateDAUs(ctx, arg) } -func (q *querier) authorizeTemplateInsights(ctx context.Context, templateIDs []uuid.UUID) error { - // Abort early if can read all template insights, aka admins. - // TODO: If we know the org, that would allow org admins to abort early too. - if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { - for _, templateID := range templateIDs { - template, err := q.db.GetTemplateByID(ctx, templateID) - if err != nil { - return err - } - - if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { - return err - } - } - if len(templateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { - return err - } - } - } - return nil -} - func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return database.GetTemplateInsightsRow{}, err diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index e2befe8b77b12..57ec0982a15ae 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -20,6 +20,7 @@ var ( // - "ActionCreate" :: create an api key // - "ActionDelete" :: delete an api key // - "ActionRead" :: read api key details (secrets are not stored) + // - "ActionUpdate" :: update an api key, eg expires ResourceApiKey = Object{ Type: "api_key", } @@ -44,6 +45,7 @@ var ( // ResourceAuditLog // Valid Actions + // - "ActionCreate" :: create new audit log entries // - "ActionRead" :: read audit logs ResourceAuditLog = Object{ Type: "audit_log", @@ -51,7 +53,7 @@ var ( // ResourceDebugInfo // Valid Actions - // - "ActionUse" :: access to debug routes + // - "ActionRead" :: access to debug routes ResourceDebugInfo = Object{ Type: "debug_info", } @@ -59,6 +61,7 @@ var ( // ResourceDeploymentConfig // Valid Actions // - "ActionRead" :: read deployment config + // - "ActionUpdate" :: updating health information ResourceDeploymentConfig = Object{ Type: "deployment_config", } @@ -141,7 +144,7 @@ var ( // - "ActionCreate" :: create an organization member // - "ActionDelete" :: delete member // - "ActionRead" :: read member - // - "ActionUpdate" :: update a organization member + // - "ActionUpdate" :: update an organization member ResourceOrganizationMember = Object{ Type: "organization_member", } @@ -199,7 +202,7 @@ var ( // - "ActionCreate" :: create a new user // - "ActionDelete" :: delete an existing user // - "ActionRead" :: read user data - // - "ActionReadPersonal" :: read personal user data like password + // - "ActionReadPersonal" :: read personal user data like user settings and auth links // - "ActionUpdate" :: update an existing user // - "ActionUpdatePersonal" :: update personal data ResourceUser = Object{ @@ -209,11 +212,12 @@ var ( // ResourceWorkspace // Valid Actions // - "ActionApplicationConnect" :: connect to workspace apps via browser - // - "ActionWorkspaceStart" :: allows starting, stopping, and updating a workspace // - "ActionCreate" :: create a new workspace // - "ActionDelete" :: delete workspace // - "ActionRead" :: read workspace data to view on the UI // - "ActionSSH" :: ssh into a given workspace + // - "ActionWorkspaceStart" :: allows starting a workspace + // - "ActionWorkspaceStop" :: allows stopping a workspace // - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters) ResourceWorkspace = Object{ Type: "workspace", @@ -222,11 +226,12 @@ var ( // ResourceWorkspaceDormant // Valid Actions // - "ActionApplicationConnect" :: connect to workspace apps via browser - // - "ActionWorkspaceStart" :: allows starting, stopping, and updating a workspace // - "ActionCreate" :: create a new workspace // - "ActionDelete" :: delete workspace // - "ActionRead" :: read workspace data to view on the UI // - "ActionSSH" :: ssh into a given workspace + // - "ActionWorkspaceStart" :: allows starting a workspace + // - "ActionWorkspaceStop" :: allows stopping a workspace // - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters) ResourceWorkspaceDormant = Object{ Type: "workspace_dormant", @@ -287,5 +292,6 @@ func AllActions() []policy.Action { policy.ActionUse, policy.ActionViewInsights, policy.ActionWorkspaceStart, + policy.ActionWorkspaceStop, } } diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 8741f8c5f22cc..9c7d9cc485128 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -45,5 +45,6 @@ const ( ActionUpdatePersonal RBACAction = "update_personal" ActionUse RBACAction = "use" ActionViewInsights RBACAction = "view_insights" - ActionWorkspaceBuild RBACAction = "build" + ActionWorkspaceStart RBACAction = "start" + ActionWorkspaceStop RBACAction = "stop" ) diff --git a/docs/api/authorization.md b/docs/api/authorization.md index 17fc2e81d2299..94f8772183d0d 100644 --- a/docs/api/authorization.md +++ b/docs/api/authorization.md @@ -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": "workspace" + "resource_type": "*" } }, "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": "workspace" + "resource_type": "*" } } } diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 68ad8c8612733..42f8f43517233 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1071,7 +1071,7 @@ "organization_id": "string", "owner_id": "string", "resource_id": "string", - "resource_type": "workspace" + "resource_type": "*" } } ``` @@ -1082,7 +1082,7 @@ AuthorizationCheck is used to check if the currently authenticated user (or the | Name | Type | Required | Restrictions | Description | | -------- | ------------------------------------------------------------ | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `action` | string | false | | | +| `action` | [codersdk.RBACAction](#codersdkrbacaction) | false | | | | `object` | [codersdk.AuthorizationObject](#codersdkauthorizationobject) | false | | Object can represent a "set" of objects, such as: all workspaces in an organization, all workspaces owned by me, and all workspaces across the entire product. When defining an object, use the most specific language when possible to produce the smallest set. Meaning to set as many fields on 'Object' as you can. Example, if you want to check if you can update all workspaces owned by 'me', try to also add an 'OrganizationID' to the settings. Omitting the 'OrganizationID' could produce the incorrect value, as workspaces have both `user` and `organization` owners. | #### Enumerated Values @@ -1101,7 +1101,7 @@ AuthorizationCheck is used to check if the currently authenticated user (or the "organization_id": "string", "owner_id": "string", "resource_id": "string", - "resource_type": "workspace" + "resource_type": "*" } ``` @@ -1127,7 +1127,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "organization_id": "string", "owner_id": "string", "resource_id": "string", - "resource_type": "workspace" + "resource_type": "*" } }, "property2": { @@ -1136,7 +1136,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "organization_id": "string", "owner_id": "string", "resource_id": "string", - "resource_type": "workspace" + "resource_type": "*" } } } @@ -3968,42 +3968,69 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `icon` | string | false | | | | `name` | string | true | | | +## codersdk.RBACAction + +```json +"application_connect" +``` + +### Properties + +#### Enumerated Values + +| Value | +| --------------------- | +| `application_connect` | +| `assign` | +| `create` | +| `delete` | +| `read` | +| `read_personal` | +| `ssh` | +| `update` | +| `update_personal` | +| `use` | +| `view_insights` | +| `start` | +| `stop` | + ## 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` | -| `user_workspace_build_parameters` | -| `organization_member` | -| `license` | -| `deployment_config` | -| `deployment_stats` | -| `replicas` | -| `debug_info` | -| `system` | -| `template_insights` | +| Value | +| ----------------------- | +| `*` | +| `api_key` | +| `assign_org_role` | +| `assign_role` | +| `audit_log` | +| `debug_info` | +| `deployment_config` | +| `deployment_stats` | +| `file` | +| `group` | +| `license` | +| `oauth2_app` | +| `oauth2_app_code_token` | +| `oauth2_app_secret` | +| `organization` | +| `organization_member` | +| `provisioner_daemon` | +| `replicas` | +| `system` | +| `tailnet_coordinator` | +| `template` | +| `user` | +| `workspace` | +| `workspace_dormant` | +| `workspace_proxy` | ## codersdk.RateLimitConfig diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 0a6a5893e581b..38f13434c77e4 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -51,7 +51,7 @@ func main() { case "rbac": source = rbacObjectTemplate default: - _, _ = fmt.Fprintf(os.Stderr, fmt.Sprintf("%q is not a valid templte target\n", flag.Args()[0])) + _, _ = fmt.Fprintf(os.Stderr, "%q is not a valid templte target\n", flag.Args()[0]) usage() os.Exit(2) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 52d62f2415526..9331339ed1aa1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2059,12 +2059,13 @@ export const ProxyHealthStatuses: ProxyHealthStatus[] = [ export type RBACAction = | "application_connect" | "assign" - | "build" | "create" | "delete" | "read" | "read_personal" | "ssh" + | "start" + | "stop" | "update" | "update_personal" | "use" @@ -2072,12 +2073,13 @@ export type RBACAction = export const RBACActions: RBACAction[] = [ "application_connect", "assign", - "build", "create", "delete", "read", "read_personal", "ssh", + "start", + "stop", "update", "update_personal", "use",