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:
Steven Masley
2024-05-15 11:09:42 -05:00
committed by GitHub
parent cb6b5e8fbd
commit 1f5788feff
48 changed files with 1809 additions and 1053 deletions

View File

@ -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
View File

@ -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": {

View File

@ -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": {

View File

@ -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:

View File

@ -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,
}

View File

@ -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,

View File

@ -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)

View File

@ -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)
}))
}

View File

@ -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 {

View File

@ -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.",
})

View File

@ -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
}

View File

@ -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
}

View File

@ -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**

View File

@ -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

View File

@ -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},
},
)

View File

@ -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) {

View File

@ -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

View File

@ -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,
}
}

View File

@ -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)
}
}

View File

@ -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(""),
},
},
}

View File

@ -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{})

View File

@ -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) {

View File

@ -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},
},
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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"),
)
}

View File

@ -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
}

View File

@ -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.

View File

@ -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

View File

@ -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.",

View File

@ -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,

View File

@ -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)
}

View 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"
)

View File

@ -25,7 +25,7 @@ curl -X POST http://coder-server:8080/api/v2/authcheck \
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "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
View File

@ -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

View File

@ -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",
})

View File

@ -59,7 +59,7 @@ func TestCheckACLPermissions(t *testing.T) {
ResourceType: codersdk.ResourceTemplate,
ResourceID: template.ID.String(),
},
Action: "write",
Action: codersdk.ActionUpdate,
},
}

View File

@ -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{},

View File

@ -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}
}

View File

@ -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{},

View 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 }}
)

View File

@ -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
}

View File

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

View 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 }}
}
}

View File

@ -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",
];

View File

@ -28,9 +28,10 @@ const templatePermissions = (
},
canReadInsights: {
object: {
resource_type: "template_insights",
resource_type: "template",
resource_id: templateId,
},
action: "read",
action: "view_insights",
},
});

View File

@ -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,
},
}