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
+1 -1
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**
+17 -5
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
+97 -91
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},
},
)
+3 -3
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) {
+31 -226
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
+275 -16
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,
}
}
+5 -5
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)
}
}
+234
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(""),
},
},
}
+49 -36
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{})
+239 -32
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) {
+4 -4
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},
},
}