mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
chore: remove rbac psuedo resources, add custom verbs (#13276)
Removes our pseudo rbac resources like `WorkspaceApplicationConnect` in favor of additional verbs like `ssh`. This is to make more intuitive permissions for building custom roles. The source of truth is now `policy.go`
This commit is contained in:
6
Makefile
6
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
|
||||
|
121
coderd/apidoc/docs.go
generated
121
coderd/apidoc/docs.go
generated
@ -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.RBACAction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"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": [
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"ResourceFile",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceOrganization",
|
||||
"ResourceRoleAssignment",
|
||||
"ResourceOrgRoleAssignment",
|
||||
"ResourceAPIKey",
|
||||
"ResourceUser",
|
||||
"ResourceUserData",
|
||||
"ResourceUserWorkspaceBuildParameters",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceLicense",
|
||||
"ResourceDeploymentValues",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceReplicas",
|
||||
"ResourceDebugInfo",
|
||||
"ResourceDeploymentConfig",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceFile",
|
||||
"ResourceGroup",
|
||||
"ResourceLicense",
|
||||
"ResourceOauth2App",
|
||||
"ResourceOauth2AppCodeToken",
|
||||
"ResourceOauth2AppSecret",
|
||||
"ResourceOrganization",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceReplicas",
|
||||
"ResourceSystem",
|
||||
"ResourceTemplateInsights"
|
||||
"ResourceTailnetCoordinator",
|
||||
"ResourceTemplate",
|
||||
"ResourceUser",
|
||||
"ResourceWorkspace",
|
||||
"ResourceWorkspaceDormant",
|
||||
"ResourceWorkspaceProxy"
|
||||
]
|
||||
},
|
||||
"codersdk.RateLimitConfig": {
|
||||
|
123
coderd/apidoc/swagger.json
generated
123
coderd/apidoc/swagger.json
generated
@ -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.RBACAction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"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": [
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"ResourceFile",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceOrganization",
|
||||
"ResourceRoleAssignment",
|
||||
"ResourceOrgRoleAssignment",
|
||||
"ResourceAPIKey",
|
||||
"ResourceUser",
|
||||
"ResourceUserData",
|
||||
"ResourceUserWorkspaceBuildParameters",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceLicense",
|
||||
"ResourceDeploymentValues",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceReplicas",
|
||||
"ResourceDebugInfo",
|
||||
"ResourceDeploymentConfig",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceFile",
|
||||
"ResourceGroup",
|
||||
"ResourceLicense",
|
||||
"ResourceOauth2App",
|
||||
"ResourceOauth2AppCodeToken",
|
||||
"ResourceOauth2AppSecret",
|
||||
"ResourceOrganization",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceReplicas",
|
||||
"ResourceSystem",
|
||||
"ResourceTemplateInsights"
|
||||
"ResourceTailnetCoordinator",
|
||||
"ResourceTemplate",
|
||||
"ResourceUser",
|
||||
"ResourceWorkspace",
|
||||
"ResourceWorkspaceDormant",
|
||||
"ResourceWorkspaceProxy"
|
||||
]
|
||||
},
|
||||
"codersdk.RateLimitConfig": {
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}))
|
||||
}
|
||||
|
@ -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,51 +154,16 @@ func (w GetWorkspaceByAgentIDRow) RBACObject() rbac.Object {
|
||||
}
|
||||
|
||||
func (w Workspace) RBACObject() rbac.Object {
|
||||
// If a workspace is locked it cannot be accessed.
|
||||
if w.DormantAt.Valid {
|
||||
return w.DormantRBAC()
|
||||
}
|
||||
|
||||
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).
|
||||
InOrg(w.OrganizationID).
|
||||
WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (w Workspace) DormantRBAC() rbac.Object {
|
||||
return rbac.ResourceWorkspaceDormant.
|
||||
WithID(w.ID).
|
||||
@ -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 {
|
||||
|
@ -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.",
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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**
|
||||
|
||||
|
@ -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
|
||||
|
@ -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},
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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{})
|
||||
|
@ -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) {
|
||||
|
@ -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},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
50
codersdk/rbacresources_gen.go
Normal file
50
codersdk/rbacresources_gen.go
Normal file
@ -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"
|
||||
)
|
4
docs/api/authorization.md
generated
4
docs/api/authorization.md
generated
@ -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": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
93
docs/api/schemas.md
generated
93
docs/api/schemas.md
generated
@ -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.RBACResource
|
||||
## codersdk.RBACAction
|
||||
|
||||
```json
|
||||
"workspace"
|
||||
"application_connect"
|
||||
```
|
||||
|
||||
### 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 |
|
||||
| --------------------- |
|
||||
| `application_connect` |
|
||||
| `assign` |
|
||||
| `create` |
|
||||
| `delete` |
|
||||
| `read` |
|
||||
| `read_personal` |
|
||||
| `ssh` |
|
||||
| `update` |
|
||||
| `update_personal` |
|
||||
| `use` |
|
||||
| `view_insights` |
|
||||
| `start` |
|
||||
| `stop` |
|
||||
|
||||
## codersdk.RBACResource
|
||||
|
||||
```json
|
||||
"*"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| 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
|
||||
|
||||
|
@ -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",
|
||||
})
|
||||
|
@ -59,7 +59,7 @@ func TestCheckACLPermissions(t *testing.T) {
|
||||
ResourceType: codersdk.ResourceTemplate,
|
||||
ResourceID: template.ID.String(),
|
||||
},
|
||||
Action: "write",
|
||||
Action: codersdk.ActionUpdate,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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{},
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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{},
|
||||
|
18
scripts/rbacgen/codersdk.gotmpl
Normal file
18
scripts/rbacgen/codersdk.gotmpl
Normal file
@ -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 }}
|
||||
)
|
@ -2,73 +2,66 @@ 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 <codersdk|rbac>")
|
||||
_, _ = 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))
|
||||
}
|
||||
|
||||
rbacPkg := pkgs[0]
|
||||
if rbacPkg.Name != "rbac" {
|
||||
log.Fatalf("Expected rbac package, got %q", rbacPkg.Name)
|
||||
}
|
||||
|
||||
tpl, err := template.New("object.gotmpl").Parse(objectGoTpl)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse templates: %s", err.Error())
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err = tpl.Execute(&out, TplState{
|
||||
ResourceNames: allResources(rbacPkg),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Execute template: %s", err.Error())
|
||||
}
|
||||
|
||||
formatted, err := format.Source(out.Bytes())
|
||||
formatted, err := format.Source(out)
|
||||
if err != nil {
|
||||
log.Fatalf("Format template: %s", err.Error())
|
||||
}
|
||||
@ -76,15 +69,146 @@ func main() {
|
||||
_, _ = fmt.Fprint(os.Stdout, string(formatted))
|
||||
}
|
||||
|
||||
func allResources(pkg *packages.Package) []string {
|
||||
var resources []string
|
||||
names := pkg.Types.Scope().Names()
|
||||
for _, name := range names {
|
||||
obj, ok := pkg.Types.Scope().Lookup(name).(*types.Var)
|
||||
if ok && obj.Type().String() == "github.com/coder/coder/v2/coderd/rbac.Object" {
|
||||
resources = append(resources, obj.Name())
|
||||
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:]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
sort.Strings(resources)
|
||||
return resources
|
||||
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 {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("parse template: %w", err)
|
||||
}
|
||||
|
||||
// Convert to sorted list for autogen consistency.
|
||||
var out bytes.Buffer
|
||||
list := make([]Definition, 0)
|
||||
for t, v := range policy.RBACPermissions {
|
||||
v := v
|
||||
list = append(list, Definition{
|
||||
PermissionDefinition: v,
|
||||
Type: t,
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(list, func(a, b Definition) int {
|
||||
return strings.Compare(a.Type, b.Type)
|
||||
})
|
||||
|
||||
err = tpl.Execute(&out, list)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute template: %w", err)
|
||||
}
|
||||
|
||||
if len(errorList) > 0 {
|
||||
return nil, errors.Join(errorList...)
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
// Code generated by rbacgen/main.go. DO NOT EDIT.
|
||||
package rbac
|
||||
|
||||
func AllResources() []Object {
|
||||
return []Object{
|
||||
{{- range .ResourceNames }}
|
||||
{{ . }},
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
||||
|
39
scripts/rbacgen/rbacobject.gotmpl
Normal file
39
scripts/rbacgen/rbacobject.gotmpl
Normal file
@ -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 }}
|
||||
}
|
||||
}
|
59
site/src/api/typesGenerated.ts
generated
59
site/src/api/typesGenerated.ts
generated
@ -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
|
||||
export type RBACResource =
|
||||
| "api_key"
|
||||
// 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"
|
||||
| "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",
|
||||
];
|
||||
|
||||
|
@ -28,9 +28,10 @@ const templatePermissions = (
|
||||
},
|
||||
canReadInsights: {
|
||||
object: {
|
||||
resource_type: "template_insights",
|
||||
resource_type: "template",
|
||||
resource_id: templateId,
|
||||
},
|
||||
action: "read",
|
||||
action: "view_insights",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user