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/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/authorize.go b/coderd/authorize.go index 9adff89769805..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,13 +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() { - case rbac.ResourceWorkspaceExecution.Type: - workSpace, err := api.Database.GetWorkspaceByID(ctx, id) - if err == nil { - dbObj = workSpace.ExecutionRBAC() - } - dbErr = err + switch string(v.Object.ResourceType) { case rbac.ResourceWorkspace.Type: dbObj, dbErr = api.Database.GetWorkspaceByID(ctx, id) case rbac.ResourceTemplate.Type: 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/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 3d9129928c811..a096346f57064 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -16,12 +16,12 @@ 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" "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" ) @@ -164,14 +164,14 @@ 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: {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.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}, @@ -192,11 +192,11 @@ 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: {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{}, @@ -214,7 +214,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,19 +234,17 @@ 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.ResourceRoleAssignment.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.ResourceOrgRoleAssignment.Type: {policy.ActionCreate}, + rbac.ResourceAssignOrgRole.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionDelete}, 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.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{}, @@ -315,6 +313,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 @@ -324,7 +336,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) } @@ -384,13 +396,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) { @@ -407,7 +420,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) } @@ -416,6 +429,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. @@ -488,6 +513,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) { @@ -504,7 +530,7 @@ func fetchWithPostFilter[ } // Authorize the action - return rbac.Filter(ctx, authorizer, act, policy.ActionRead, objects) + return rbac.Filter(ctx, authorizer, act, action, objects) } } @@ -560,7 +586,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) @@ -585,7 +611,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 } } @@ -655,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) } @@ -731,7 +780,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 } @@ -755,7 +804,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 } @@ -770,14 +819,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 { @@ -804,7 +853,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) @@ -823,14 +872,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) @@ -838,7 +887,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) @@ -950,15 +999,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) { @@ -1078,11 +1127,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) { @@ -1125,7 +1174,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) { @@ -1144,11 +1193,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) { @@ -1213,7 +1262,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) { @@ -1227,7 +1276,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) @@ -1242,7 +1291,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) @@ -1253,7 +1302,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) @@ -1269,14 +1318,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) @@ -1285,7 +1334,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) @@ -1309,7 +1358,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) { @@ -1317,18 +1366,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) { @@ -1370,7 +1419,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) { @@ -1496,31 +1545,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) @@ -1551,101 +1584,37 @@ func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateD } 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 { - template, err := q.db.GetTemplateByID(ctx, templateID) - if err != nil { - return database.GetTemplateInsightsRow{}, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { - return database.GetTemplateInsightsRow{}, err - } - } - if len(arg.TemplateIDs) == 0 { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { - return database.GetTemplateInsightsRow{}, err - } - } + 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) } @@ -1803,19 +1772,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); 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 } } @@ -1840,19 +1809,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 } } @@ -1886,7 +1855,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) @@ -2143,7 +2116,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) } @@ -2277,7 +2250,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) } @@ -2312,7 +2285,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) { @@ -2320,7 +2293,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) { @@ -2349,7 +2322,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) @@ -2357,14 +2330,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) @@ -2375,7 +2348,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) @@ -2561,12 +2534,14 @@ 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.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.WorkspaceBuildRBAC(arg.Transition)); err != nil { + if err = q.authorizeContext(ctx, action, w); err != nil { return xerrors.Errorf("authorize context: %w", err) } @@ -2719,14 +2694,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) { @@ -2765,14 +2740,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) @@ -2996,7 +2971,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) @@ -3012,10 +2987,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 } @@ -3038,7 +3013,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) { @@ -3060,7 +3035,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) @@ -3071,7 +3046,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) @@ -3310,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.ResourceDeploymentValues); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertApplicationName(ctx, value) @@ -3324,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.ResourceDeploymentValues); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertHealthSettings(ctx, value) @@ -3359,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.ResourceDeploymentValues); 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.ResourceDeploymentValues); 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 92dbbb8e7bce1..e8dcb2f8ee5bc 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.ActionUpdate) })) 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.ActionUpdate) })) 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.ActionAssign, 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.ActionAssign, // 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.ActionAssign, 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.ActionAssign, + rbac.ResourceAssignRole, policy.ActionDelete, ).Returns(o) })) s.Run("AllUserIDs", s.Subtest(func(db database.Store, check *expects) { @@ -1430,7 +1432,18 @@ func (s *MethodTestSuite) TestWorkspace() { WorkspaceID: w.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, - }).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), policy.ActionUpdate) + }).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{}) @@ -1452,7 +1465,7 @@ func (s *MethodTestSuite) TestWorkspace() { Reason: database.BuildReasonInitiator, TemplateVersionID: v.ID, }).Asserts( - w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), policy.ActionUpdate, + w, policy.ActionWorkspaceStart, t, policy.ActionUpdate, ) })) @@ -1480,7 +1493,7 @@ func (s *MethodTestSuite) TestWorkspace() { Reason: database.BuildReasonInitiator, TemplateVersionID: v.ID, }).Asserts( - w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), policy.ActionUpdate, + w, policy.ActionWorkspaceStart, ) })) s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { @@ -1489,7 +1502,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 +2217,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.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.ResourceDeploymentValues, 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() @@ -2335,11 +2348,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 +2370,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 +2383,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 +2394,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 +2418,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 +2449,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 +2485,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 +2508,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 +2525,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 +2540,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 +2560,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 9e7777283967d..d71c63b089556 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,47 +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). - InOrg(w.OrganizationID). - 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). + return rbac.ResourceWorkspace.WithID(w.ID). InOrg(w.OrganizationID). WithOwner(w.OwnerID.String()) } @@ -246,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{ @@ -281,25 +231,20 @@ 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)) } 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/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/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** diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index c647bb09f89a0..859782d0286b1 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 @@ -219,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) @@ -240,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 @@ -326,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_internal_test.go b/coderd/rbac/authz_internal_test.go index cba69952ea481..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" ) @@ -303,16 +304,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}, + user.ID: {policy.WildcardSymbol}, }), - actions: AllActions(), + actions: ResourceWorkspace.AvailableActions(), allow: true, }, { @@ -335,16 +336,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}, + allUsersGroup: {policy.WildcardSymbol}, }), - actions: AllActions(), + actions: ResourceWorkspace.AvailableActions(), allow: true, }, { @@ -366,27 +367,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{ @@ -398,8 +399,8 @@ func TestAuthorizeDomain(t *testing.T) { Site: []Permission{ { Negate: true, - ResourceType: WildcardSymbol, - Action: WildcardSymbol, + ResourceType: policy.WildcardSymbol, + Action: policy.WildcardSymbol, }, }, }}, @@ -407,27 +408,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{ @@ -439,29 +440,33 @@ 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: 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: workspaceExceptConnect, allow: true}, + {resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceConnect, 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: 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: 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 +480,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 +515,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 { @@ -713,8 +718,8 @@ func TestAuthorizeLevels(t *testing.T) { User: []Permission{ { Negate: true, - ResourceType: WildcardSymbol, - Action: WildcardSymbol, + ResourceType: policy.WildcardSymbol, + Action: policy.WildcardSymbol, }, }, }, @@ -723,7 +728,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{ @@ -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 = AllActions() + // SSH and app connect are not implied here. + c.actions = slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH) return c }, []authTestCase{ // Org + me @@ -840,9 +846,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 +881,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/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/object.go b/coderd/rbac/object.go index bac8b90fe90c4..30a74e4f825dd 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -1,237 +1,13 @@ package rbac import ( + "fmt" + "github.com/google/uuid" "github.com/coder/coder/v2/coderd/rbac/policy" ) -const WildcardSymbol = "*" - -// Objecter returns the RBAC object for itself. -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()) @@ -256,6 +32,35 @@ 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 for type %q", action, z.Type) + } + + return nil +} + +// AvailableActions returns all available actions for a given object. +// Wildcard is omitted. +func (z Object) AvailableActions() []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 { if z.ID != b.ID { return false diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index b1cac5704e049..57ec0982a15ae 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -1,38 +1,297 @@ // Code generated by rbacgen/main.go. DO NOT EDIT. package rbac -func AllResources() []Object { - return []Object{ - ResourceAPIKey, +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 + ResourceWildcard = Object{ + Type: "*", + } + + // ResourceApiKey + // Valid Actions + // - "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", + } + + // ResourceAssignOrgRole + // Valid Actions + // - "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" :: ability to assign roles + // - "ActionDelete" :: ability to delete roles + // - "ActionRead" :: view what roles are assignable + ResourceAssignRole = Object{ + Type: "assign_role", + } + + // ResourceAuditLog + // Valid Actions + // - "ActionCreate" :: create new audit log entries + // - "ActionRead" :: read audit logs + ResourceAuditLog = Object{ + Type: "audit_log", + } + + // ResourceDebugInfo + // Valid Actions + // - "ActionRead" :: access to debug routes + ResourceDebugInfo = Object{ + Type: "debug_info", + } + + // ResourceDeploymentConfig + // Valid Actions + // - "ActionRead" :: read deployment config + // - "ActionUpdate" :: updating health information + ResourceDeploymentConfig = Object{ + Type: "deployment_config", + } + + // ResourceDeploymentStats + // Valid Actions + // - "ActionRead" :: read deployment stats + ResourceDeploymentStats = Object{ + Type: "deployment_stats", + } + + // ResourceFile + // Valid Actions + // - "ActionCreate" :: create a file + // - "ActionRead" :: read files + ResourceFile = Object{ + Type: "file", + } + + // ResourceGroup + // Valid Actions + // - "ActionCreate" :: create a group + // - "ActionDelete" :: delete a group + // - "ActionRead" :: read groups + // - "ActionUpdate" :: update a group + ResourceGroup = Object{ + Type: "group", + } + + // ResourceLicense + // Valid Actions + // - "ActionCreate" :: create a license + // - "ActionDelete" :: delete license + // - "ActionRead" :: read licenses + ResourceLicense = Object{ + Type: "license", + } + + // ResourceOauth2App + // Valid Actions + // - "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" :: + // - "ActionDelete" :: + // - "ActionRead" :: + ResourceOauth2AppCodeToken = Object{ + Type: "oauth2_app_code_token", + } + + // ResourceOauth2AppSecret + // Valid Actions + // - "ActionCreate" :: + // - "ActionDelete" :: + // - "ActionRead" :: + // - "ActionUpdate" :: + ResourceOauth2AppSecret = Object{ + Type: "oauth2_app_secret", + } + + // ResourceOrganization + // Valid Actions + // - "ActionCreate" :: create an organization + // - "ActionDelete" :: delete an organization + // - "ActionRead" :: read organizations + // - "ActionUpdate" :: update an organization + ResourceOrganization = Object{ + Type: "organization", + } + + // ResourceOrganizationMember + // Valid Actions + // - "ActionCreate" :: create an organization member + // - "ActionDelete" :: delete member + // - "ActionRead" :: read member + // - "ActionUpdate" :: update an organization member + ResourceOrganizationMember = Object{ + Type: "organization_member", + } + + // ResourceProvisionerDaemon + // Valid Actions + // - "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" :: read replicas + ResourceReplicas = Object{ + Type: "replicas", + } + + // ResourceSystem + // Valid Actions + // - "ActionCreate" :: create system resources + // - "ActionDelete" :: delete system resources + // - "ActionRead" :: view system resources + // - "ActionUpdate" :: update system resources + ResourceSystem = Object{ + Type: "system", + } + + // ResourceTailnetCoordinator + // Valid Actions + // - "ActionCreate" :: + // - "ActionDelete" :: + // - "ActionRead" :: + // - "ActionUpdate" :: + ResourceTailnetCoordinator = Object{ + Type: "tailnet_coordinator", + } + + // ResourceTemplate + // Valid Actions + // - "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" :: create a new user + // - "ActionDelete" :: delete an existing user + // - "ActionRead" :: read user data + // - "ActionReadPersonal" :: read personal user data like user settings and auth links + // - "ActionUpdate" :: update an existing user + // - "ActionUpdatePersonal" :: update personal data + ResourceUser = Object{ + Type: "user", + } + + // ResourceWorkspace + // Valid Actions + // - "ActionApplicationConnect" :: connect to workspace apps via browser + // - "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", + } + + // ResourceWorkspaceDormant + // Valid Actions + // - "ActionApplicationConnect" :: connect to workspace apps via browser + // - "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", + } + + // ResourceWorkspaceProxy + // Valid Actions + // - "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", + } +) + +func AllResources() []Objecter { + return []Objecter{ + ResourceWildcard, + ResourceApiKey, + ResourceAssignOrgRole, + ResourceAssignRole, ResourceAuditLog, ResourceDebugInfo, + ResourceDeploymentConfig, ResourceDeploymentStats, - ResourceDeploymentValues, ResourceFile, ResourceGroup, ResourceLicense, - ResourceOAuth2ProviderApp, - ResourceOAuth2ProviderAppCodeToken, - ResourceOAuth2ProviderAppSecret, - ResourceOrgRoleAssignment, + ResourceOauth2App, + ResourceOauth2AppCodeToken, + ResourceOauth2AppSecret, ResourceOrganization, ResourceOrganizationMember, ResourceProvisionerDaemon, ResourceReplicas, - ResourceRoleAssignment, ResourceSystem, ResourceTailnetCoordinator, ResourceTemplate, - ResourceTemplateInsights, ResourceUser, - ResourceUserData, - ResourceUserWorkspaceBuildParameters, - ResourceWildcard, ResourceWorkspace, - ResourceWorkspaceApplicationConnect, - ResourceWorkspaceBuild, ResourceWorkspaceDormant, - ResourceWorkspaceExecution, ResourceWorkspaceProxy, } } + +func AllActions() []policy.Action { + return []policy.Action{ + policy.ActionApplicationConnect, + policy.ActionAssign, + policy.ActionCreate, + policy.ActionDelete, + policy.ActionRead, + policy.ActionReadPersonal, + policy.ActionSSH, + policy.ActionUpdate, + policy.ActionUpdatePersonal, + policy.ActionUse, + policy.ActionViewInsights, + policy.ActionWorkspaceStart, + policy.ActionWorkspaceStop, + } +} 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 a3c0dc9f3436b..26afb0e011ca7 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -1,5 +1,7 @@ package policy +const WildcardSymbol = "*" + // Action represents the allowed actions to be done on an object. type Action string @@ -8,4 +10,236 @@ const ( ActionRead Action = "read" ActionUpdate Action = "update" ActionDelete Action = "delete" + + ActionUse Action = "use" + ActionSSH Action = "ssh" + ActionApplicationConnect Action = "application_connect" + ActionViewInsights Action = "view_insights" + + ActionWorkspaceStart Action = "start" + ActionWorkspaceStop Action = "stop" + + ActionAssign Action = "assign" + + ActionReadPersonal Action = "read_personal" + ActionUpdatePersonal Action = "update_personal" ) + +type PermissionDefinition struct { + // name is optional. Used to override "Type" for function naming. + 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 +} + +type ActionDefinition struct { + // Human friendly description to explain the action. + Description string +} + +func actDef(description string) ActionDefinition { + return ActionDefinition{ + Description: description, + } +} + +var workspaceActions = map[Action]ActionDefinition{ + ActionCreate: actDef("create a new workspace"), + ActionRead: actDef("read workspace data to view on the UI"), + // TODO: Make updates more granular + ActionUpdate: actDef("edit workspace settings (scheduling, permissions, parameters)"), + ActionDelete: actDef("delete 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"), + ActionApplicationConnect: actDef("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. + // So can grant all actions on all types. + WildcardSymbol: { + Name: "Wildcard", + Actions: map[Action]ActionDefinition{}, + }, + "user": { + Actions: map[Action]ActionDefinition{ + // Actions deal with site wide user objects. + 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("read personal user data like user settings and auth links"), + ActionUpdatePersonal: actDef("update personal data"), + }, + }, + "workspace": { + Actions: workspaceActions, + }, + // Dormant workspaces have the same perms as workspaces. + "workspace_dormant": { + Actions: workspaceActions, + }, + "workspace_proxy": { + Actions: map[Action]ActionDefinition{ + 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("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("read audit logs"), + ActionCreate: actDef("create new audit log entries"), + }, + }, + "deployment_config": { + Actions: map[Action]ActionDefinition{ + ActionRead: actDef("read deployment config"), + ActionUpdate: actDef("updating health information"), + }, + }, + "deployment_stats": { + Actions: map[Action]ActionDefinition{ + ActionRead: actDef("read deployment stats"), + }, + }, + "replicas": { + Actions: map[Action]ActionDefinition{ + ActionRead: actDef("read replicas"), + }, + }, + "template": { + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef("create a template"), + // TODO: Create a use permission maybe? + 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("create a group"), + ActionRead: actDef("read groups"), + ActionDelete: actDef("delete a group"), + ActionUpdate: actDef("update a group"), + }, + }, + "file": { + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef("create a file"), + ActionRead: actDef("read files"), + }, + }, + "provisioner_daemon": { + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef("create a provisioner daemon"), + // TODO: Move to use? + ActionRead: actDef("read provisioner daemon"), + ActionUpdate: actDef("update a provisioner daemon"), + ActionDelete: actDef("delete a provisioner daemon"), + }, + }, + "organization": { + Actions: map[Action]ActionDefinition{ + 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("create an organization member"), + ActionRead: actDef("read member"), + ActionUpdate: actDef("update an organization member"), + ActionDelete: actDef("delete member"), + }, + }, + "debug_info": { + Actions: map[Action]ActionDefinition{ + ActionRead: actDef("access to debug routes"), + }, + }, + "system": { + Actions: map[Action]ActionDefinition{ + 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("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": { + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(""), + ActionRead: actDef(""), + ActionUpdate: actDef(""), + ActionDelete: actDef(""), + }, + }, + "assign_role": { + Actions: map[Action]ActionDefinition{ + 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("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("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(""), + ActionRead: actDef(""), + ActionUpdate: actDef(""), + ActionDelete: actDef(""), + }, + }, + "oauth2_app_code_token": { + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef(""), + ActionRead: actDef(""), + ActionDelete: actDef(""), + }, + }, +} diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index f69cf49174f60..cee365d06624c 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,28 +71,28 @@ 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, - Action: WildcardSymbol, + ResourceType: r.RBACObject().Type, + Action: policy.WildcardSymbol, }) } return perms @@ -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, + policy.ActionApplicationConnect, policy.ActionSSH) } // Static roles that never change should be allocated in a closure. @@ -138,30 +139,41 @@ 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, + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop}, + })...), + Org: map[string][]Permission{}, + User: []Permission{}, }.withCachedRegoValue() memberRole := Role{ Name: member, DisplayName: "Member", Site: Permissions(map[string][]policy.Action{ - ResourceRoleAssignment.Type: {policy.ActionRead}, + ResourceAssignRole.Type: {policy.ActionRead}, // All users can see the provisioner daemons. ResourceProvisionerDaemon.Type: {policy.ActionRead}, // All users can see OAuth2 provider applications. - ResourceOAuth2ProviderApp.Type: {policy.ActionRead}, + ResourceOauth2App.Type: {policy.ActionRead}, + ResourceWorkspaceProxy.Type: {policy.ActionRead}, }), 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, policy.ActionWorkspaceStop}, + // Users cannot do create/update/delete on themselves, but they // can read their own details. - ResourceUser.Type: {policy.ActionRead}, - ResourceUserWorkspaceBuildParameters.Type: {policy.ActionRead}, + ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, // Users can create provisioner daemons scoped to themselves. - ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, + ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, })..., ), }.withCachedRegoValue() @@ -172,14 +184,13 @@ 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, policy.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}, }), @@ -191,9 +202,9 @@ 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: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights}, // CRUD all files, even those they did not upload. - ResourceFile.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, ResourceWorkspace.Type: {policy.ActionRead}, // CRUD to provisioner daemons for now. ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, @@ -203,8 +214,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceGroup.Type: {policy.ActionRead}, // Org roles are not really used yet, so grant the perm at the site level. ResourceOrganizationMember.Type: {policy.ActionRead}, - // Template admins can read all template insights data - ResourceTemplateInsights.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -214,10 +223,11 @@ 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}, + ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead}, + ResourceUser.Type: { + policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, + policy.ActionUpdatePersonal, policy.ActionReadPersonal, + }, // 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}, @@ -261,7 +271,10 @@ 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{ + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop}, + ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), + })...), }, User: []Permission{}, } @@ -283,7 +296,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { }, { // Can read available roles. - ResourceType: ResourceOrgRoleAssignment.Type, + ResourceType: ResourceAssignOrgRole.Type, Action: policy.ActionRead, }, }, @@ -523,7 +536,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{}) diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index b5e78e606b8d4..44ef83b74cd20 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -34,10 +34,10 @@ 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.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") }) @@ -47,20 +47,22 @@ 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.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") }) } -// TODO: add the SYSTEM to the MATRIX +// nolint:tparallel,paralleltest -- subtests share a map, just run sequentially. func TestRolePermissions(t *testing.T) { t.Parallel() - auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry()) + crud := []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} + + auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) // currentUser is anything that references "me", "mine", or "my". currentUser := uuid.New() @@ -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}, @@ -263,8 +265,8 @@ 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()), + 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}, 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{ @@ -321,7 +332,16 @@ func TestRolePermissions(t *testing.T) { }, { Name: "WorkspaceDormant", - Actions: rbac.AllActions(), + 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}, + false: {userAdmin, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, + }, + }, + { + Name: "WorkspaceDormantUse", + 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: {}, @@ -330,25 +350,198 @@ 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.ActionWorkspaceStart, policy.ActionWorkspaceStop}, + 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, policy.ActionUpdate}, + Resource: rbac.ResourceDeploymentConfig, + AuthorizeMap: map[bool][]authSubject{ + true: {owner}, + false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + }, + }, + { + Name: "DebugInfo", + Actions: []policy.Action{policy.ActionRead}, + 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, policy.ActionCreate}, + 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: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, + 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 + } } + passed := true + // nolint:tparallel,paralleltest for _, c := range testCases { c := c + // nolint:tparallel,paralleltest -- 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{}{} } 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) @@ -359,11 +552,13 @@ 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)) + 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)) } } } @@ -371,6 +566,18 @@ func TestRolePermissions(t *testing.T) { require.Empty(t, remainingSubjs, "test should cover all subjects") }) } + + // 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,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) + } + }) + } + } } func TestIsOrgRole(t *testing.T) { diff --git a/coderd/rbac/scopes.go b/coderd/rbac/scopes.go index 6353ca3c67919..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: { @@ -74,12 +74,12 @@ 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{}, }, - AllowIDList: []string{WildcardSymbol}, + AllowIDList: []string{policy.WildcardSymbol}, }, } diff --git a/coderd/roles.go b/coderd/roles.go index 1cc74535119e3..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.ResourceRoleAssignment) { + 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.ResourceOrgRoleAssignment.InOrg(organization.ID)) { + if !api.Authorize(r, policy.ActionRead, rbac.ResourceAssignOrgRole.InOrg(organization.ID)) { httpapi.ResourceNotFound(rw) return } diff --git a/coderd/users.go b/coderd/users.go index c698661d71429..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.UserDataRBACObject()) { + if !api.Authorize(r, policy.ActionReadPersonal, 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/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/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/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/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/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/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..9c7d9cc485128 --- /dev/null +++ b/codersdk/rbacresources_gen.go @@ -0,0 +1,50 @@ +// 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" +) + +type RBACAction string + +const ( + 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" + 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/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/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 feddcce4d8372..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" ) @@ -310,7 +309,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 } @@ -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/codersdk.gotmpl b/scripts/rbacgen/codersdk.gotmpl new file mode 100644 index 0000000000000..1492eaf86c2bf --- /dev/null +++ b/scripts/rbacgen/codersdk.gotmpl @@ -0,0 +1,18 @@ +// 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 }} +) + +type RBACAction string + +const ( + {{- range $element := actionsList }} + {{ $element.Enum }} RBACAction = "{{ $element.Value }}" + {{- end }} +) diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index d237227f693dc..38f13434c77e4 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -2,89 +2,213 @@ package main import ( "bytes" - "context" _ "embed" + "errors" + "flag" "fmt" + "go/ast" "go/format" - "go/types" + "go/parser" + "go/token" "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 +//go:embed rbacobject.gotmpl +var rbacObjectTemplate string -type TplState struct { - ResourceNames []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() - path := "." - if len(os.Args) > 1 { - path = os.Args[1] + if len(flag.Args()) < 1 { + usage() + os.Exit(1) } - cfg := &packages.Config{ - Mode: packages.NeedTypes | packages.NeedName | packages.NeedTypesInfo | packages.NeedDeps, - Tests: false, - Context: ctx, + // 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": + source = codersdkTemplate + case "rbac": + source = rbacObjectTemplate + default: + _, _ = fmt.Fprintf(os.Stderr, "%q is not a valid templte target\n", flag.Args()[0]) + usage() + os.Exit(2) } - pkgs, err := packages.Load(cfg, path) + out, err := generateRbacObjects(source) if err != nil { - log.Fatalf("Failed to load package: %s", err.Error()) + log.Fatalf("Generate source: %s", err.Error()) } - if len(pkgs) != 1 { - log.Fatalf("Expected 1 package, got %d", len(pkgs)) + formatted, err := format.Source(out) + if err != nil { + log.Fatalf("Format template: %s", err.Error()) } - rbacPkg := pkgs[0] - if rbacPkg.Name != "rbac" { - log.Fatalf("Expected rbac package, got %q", rbacPkg.Name) + _, _ = fmt.Fprint(os.Stdout, string(formatted)) +} + +func pascalCaseName[T ~string](name T) string { + names := strings.Split(string(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) +type Definition struct { + policy.PermissionDefinition + Type string +} + +func (p Definition) FunctionName() string { + if p.Name != "" { + return p.Name + } + 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) + + // 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 +} + +type ActionDetails struct { + Enum string + 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) if err != nil { - log.Fatalf("Failed to parse templates: %s", err.Error()) + 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, + }) } - var out bytes.Buffer - err = tpl.Execute(&out, TplState{ - ResourceNames: allResources(rbacPkg), + // Sorting actions for auto gen consistency. + 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() []ActionDetails { + return actionList + }, + "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(templateSource) if err != nil { - log.Fatalf("Execute template: %s", err.Error()) + return nil, fmt.Errorf("parse template: %w", err) } - formatted, err := format.Source(out.Bytes()) - if err != nil { - log.Fatalf("Format template: %s", err.Error()) + // Convert to sorted list for autogen consistency. + var out bytes.Buffer + list := make([]Definition, 0) + for t, v := range policy.RBACPermissions { + v := v + list = append(list, Definition{ + PermissionDefinition: v, + Type: t, + }) } - _, _ = fmt.Fprint(os.Stdout, string(formatted)) -} + slices.SortFunc(list, func(a, b Definition) int { + return strings.Compare(a.Type, b.Type) + }) -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()) - } + err = tpl.Execute(&out, list) + if err != nil { + return nil, fmt.Errorf("execute template: %w", err) + } + + if len(errorList) > 0 { + return nil, errors.Join(errorList...) } - sort.Strings(resources) - return resources + + return out.Bytes(), nil } diff --git a/scripts/rbacgen/object.gotmpl b/scripts/rbacgen/object.gotmpl deleted file mode 100644 index 281acbc581925..0000000000000 --- a/scripts/rbacgen/object.gotmpl +++ /dev/null @@ -1,12 +0,0 @@ -// Code generated by rbacgen/main.go. DO NOT EDIT. -package rbac - -func AllResources() []Object { - return []Object{ - {{- range .ResourceNames }} - {{ . }}, - {{- end }} - } -} - - diff --git a/scripts/rbacgen/rbacobject.gotmpl b/scripts/rbacgen/rbacobject.gotmpl new file mode 100644 index 0000000000000..9e529d2986817 --- /dev/null +++ b/scripts/rbacgen/rbacobject.gotmpl @@ -0,0 +1,39 @@ +// 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 := . }} + {{- $Name := pascalCaseName $element.FunctionName }} + // Resource{{ $Name }} + // Valid Actions + {{- range $action, $value := .Actions }} + // - "{{ actionEnum $action }}" :: {{ $value.Description }} + {{- end }} + Resource{{ $Name }} = Object { + Type: "{{ $element.Type }}", + } + {{ end -}} +) + +func AllResources() []Objecter { + return []Objecter{ + {{- range $element := . }} + Resource{{ pascalCaseName $element.FunctionName }}, + {{- end }} + } +} + +func AllActions() []policy.Action { + return []policy.Action { + {{- range $element := actionsList }} + policy.{{ $element.Enum }}, + {{- end }} + } +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 8d49bc6ca7223..9331339ed1aa1 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,41 @@ export const ProxyHealthStatuses: ProxyHealthStatus[] = [ "unregistered", ]; -// From codersdk/rbacresources.go +// From codersdk/rbacresources_gen.go +export type RBACAction = + | "application_connect" + | "assign" + | "create" + | "delete" + | "read" + | "read_personal" + | "ssh" + | "start" + | "stop" + | "update" + | "update_personal" + | "use" + | "view_insights"; +export const RBACActions: RBACAction[] = [ + "application_connect", + "assign", + "create", + "delete", + "read", + "read_personal", + "ssh", + "start", + "stop", + "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 +2099,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 +2125,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", }, }); diff --git a/support/support.go b/support/support.go index e49f95e38d045..af3ad21200d02 100644 --- a/support/support.go +++ b/support/support.go @@ -10,18 +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/coder/coder/v2/coderd/healthcheck/derphealth" - "github.com/coder/coder/v2/coderd/rbac/policy" - - "github.com/google/uuid" - "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" @@ -460,9 +457,9 @@ 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), + Action: codersdk.ActionRead, }, }