mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: remove rbac psuedo resources, add custom verbs (#13276)
Removes our pseudo rbac resources like `WorkspaceApplicationConnect` in favor of additional verbs like `ssh`. This is to make more intuitive permissions for building custom roles. The source of truth is now `policy.go`
This commit is contained in:
@@ -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
@@ -26,11 +26,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
)
|
||||
|
||||
// AllActions is a helper function to return all the possible actions types.
|
||||
func AllActions() []policy.Action {
|
||||
return []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
|
||||
}
|
||||
|
||||
type AuthCall struct {
|
||||
Actor Subject
|
||||
Action policy.Action
|
||||
@@ -219,6 +214,10 @@ type RegoAuthorizer struct {
|
||||
|
||||
authorizeHist *prometheus.HistogramVec
|
||||
prepareHist prometheus.Histogram
|
||||
|
||||
// strict checking also verifies the inputs to the authorizer. Making sure
|
||||
// the action make sense for the input object.
|
||||
strict bool
|
||||
}
|
||||
|
||||
var _ Authorizer = (*RegoAuthorizer)(nil)
|
||||
@@ -240,6 +239,13 @@ func NewCachingAuthorizer(registry prometheus.Registerer) Authorizer {
|
||||
return Cacher(NewAuthorizer(registry))
|
||||
}
|
||||
|
||||
// NewStrictCachingAuthorizer is mainly just for testing.
|
||||
func NewStrictCachingAuthorizer(registry prometheus.Registerer) Authorizer {
|
||||
auth := NewAuthorizer(registry)
|
||||
auth.strict = true
|
||||
return Cacher(auth)
|
||||
}
|
||||
|
||||
func NewAuthorizer(registry prometheus.Registerer) *RegoAuthorizer {
|
||||
queryOnce.Do(func() {
|
||||
var err error
|
||||
@@ -326,6 +332,12 @@ type authSubject struct {
|
||||
// the object.
|
||||
// If an error is returned, the authorization is denied.
|
||||
func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error {
|
||||
if a.strict {
|
||||
if err := object.ValidAction(action); err != nil {
|
||||
return xerrors.Errorf("strict authz check: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
ctx, span := tracing.StartSpan(ctx,
|
||||
trace.WithTimestamp(start), // Reuse the time.Now for metric and trace
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/rbac/regosql"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -303,16 +304,16 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
testAuthorize(t, "UserACLList", user, []authTestCase{
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
|
||||
user.ID: AllActions(),
|
||||
user.ID: ResourceWorkspace.AvailableActions(),
|
||||
}),
|
||||
actions: AllActions(),
|
||||
actions: ResourceWorkspace.AvailableActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
|
||||
user.ID: {WildcardSymbol},
|
||||
user.ID: {policy.WildcardSymbol},
|
||||
}),
|
||||
actions: AllActions(),
|
||||
actions: ResourceWorkspace.AvailableActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
@@ -335,16 +336,16 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
testAuthorize(t, "GroupACLList", user, []authTestCase{
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
|
||||
allUsersGroup: AllActions(),
|
||||
allUsersGroup: ResourceWorkspace.AvailableActions(),
|
||||
}),
|
||||
actions: AllActions(),
|
||||
actions: ResourceWorkspace.AvailableActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
|
||||
allUsersGroup: {WildcardSymbol},
|
||||
allUsersGroup: {policy.WildcardSymbol},
|
||||
}),
|
||||
actions: AllActions(),
|
||||
actions: ResourceWorkspace.AvailableActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
@@ -366,27 +367,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
|
||||
testAuthorize(t, "Member", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + other us
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
@@ -398,8 +399,8 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
Site: []Permission{
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: WildcardSymbol,
|
||||
Action: WildcardSymbol,
|
||||
ResourceType: policy.WildcardSymbol,
|
||||
Action: policy.WildcardSymbol,
|
||||
},
|
||||
},
|
||||
}},
|
||||
@@ -407,27 +408,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
|
||||
testAuthorize(t, "DeletedMember", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
@@ -439,29 +440,33 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
workspaceExceptConnect := slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH)
|
||||
workspaceConnect := []policy.Action{policy.ActionApplicationConnect, policy.ActionSSH}
|
||||
testAuthorize(t, "OrgAdmin", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceExceptConnect, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceConnect, allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: workspaceExceptConnect, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: workspaceConnect, allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
@@ -475,27 +480,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
|
||||
testAuthorize(t, "SiteAdmin", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
@@ -510,60 +515,60 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
testAuthorize(t, "ApplicationToken", user,
|
||||
// Create (connect) Actions
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.actions = []policy.Action{policy.ActionCreate}
|
||||
c.actions = []policy.Action{policy.ActionApplicationConnect}
|
||||
return c
|
||||
}, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), allow: true},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), allow: false},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.WithOwner(user.ID), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), allow: true},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.All(), allow: false},
|
||||
{resource: ResourceWorkspace.All(), allow: false},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner(user.ID), allow: false},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: false},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me"), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner("not-me"), allow: false},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me"), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
}),
|
||||
// Not create actions
|
||||
// No ActionApplicationConnect action
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.actions = []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
|
||||
c.allow = false
|
||||
return c
|
||||
}, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID)},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg)},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID)},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg)},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.WithOwner(user.ID)},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID)},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.All()},
|
||||
{resource: ResourceWorkspace.All()},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner(user.ID)},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID)},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID)},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me")},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me")},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me")},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID).WithOwner("not-me")},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unuseID)},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
{resource: ResourceWorkspaceApplicationConnect.WithOwner("not-me")},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me")},
|
||||
}),
|
||||
// Other Objects
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
@@ -713,8 +718,8 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
User: []Permission{
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: WildcardSymbol,
|
||||
Action: WildcardSymbol,
|
||||
ResourceType: policy.WildcardSymbol,
|
||||
Action: policy.WildcardSymbol,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -723,7 +728,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
|
||||
testAuthorize(t, "AdminAlwaysAllow", user,
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.actions = AllActions()
|
||||
c.actions = ResourceWorkspace.AvailableActions()
|
||||
c.allow = true
|
||||
return c
|
||||
}, []authTestCase{
|
||||
@@ -761,7 +766,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: "random",
|
||||
Action: WildcardSymbol,
|
||||
Action: policy.WildcardSymbol,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -772,8 +777,8 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
User: []Permission{
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: WildcardSymbol,
|
||||
Action: WildcardSymbol,
|
||||
ResourceType: policy.WildcardSymbol,
|
||||
Action: policy.WildcardSymbol,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -782,7 +787,8 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
|
||||
testAuthorize(t, "OrgAllowAll", user,
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.actions = AllActions()
|
||||
// SSH and app connect are not implied here.
|
||||
c.actions = slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH)
|
||||
return c
|
||||
}, []authTestCase{
|
||||
// Org + me
|
||||
@@ -840,9 +846,9 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
}),
|
||||
// Allowed by scope:
|
||||
[]authTestCase{
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: true},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}, allow: true},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -875,9 +881,9 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
}),
|
||||
// Allowed by scope:
|
||||
[]authTestCase{
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}, allow: true},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false},
|
||||
{resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionApplicationConnect}, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionApplicationConnect}, allow: false},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ func BenchmarkRBACAuthorize(b *testing.B) {
|
||||
|
||||
// There is no caching that occurs because a fresh context is used for each
|
||||
// call. And the context needs 'WithCacheCtx' to work.
|
||||
authorizer := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
|
||||
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
// This benchmarks all the simple cases using just user permissions. Groups
|
||||
// are added as noise, but do not do anything.
|
||||
for _, c := range benchCases {
|
||||
@@ -187,7 +187,7 @@ func BenchmarkRBACAuthorizeGroups(b *testing.B) {
|
||||
uuid.MustParse("0632b012-49e0-4d70-a5b3-f4398f1dcd52"),
|
||||
uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"),
|
||||
)
|
||||
authorizer := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
|
||||
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
// Same benchmark cases, but this time groups will be used to match.
|
||||
// Some '*' permissions will still match, but using a fake action reduces
|
||||
@@ -239,7 +239,7 @@ func BenchmarkRBACFilter(b *testing.B) {
|
||||
uuid.MustParse("70dbaa7a-ea9c-4f68-a781-97b08af8461d"),
|
||||
)
|
||||
|
||||
authorizer := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
|
||||
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
for _, c := range benchCases {
|
||||
b.Run("PrepareOnly-"+c.Name, func(b *testing.B) {
|
||||
|
||||
+31
-226
@@ -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
@@ -1,38 +1,297 @@
|
||||
// Code generated by rbacgen/main.go. DO NOT EDIT.
|
||||
package rbac
|
||||
|
||||
func AllResources() []Object {
|
||||
return []Object{
|
||||
ResourceAPIKey,
|
||||
import "github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
|
||||
// Objecter returns the RBAC object for itself.
|
||||
type Objecter interface {
|
||||
RBACObject() Object
|
||||
}
|
||||
|
||||
var (
|
||||
// ResourceWildcard
|
||||
// Valid Actions
|
||||
ResourceWildcard = Object{
|
||||
Type: "*",
|
||||
}
|
||||
|
||||
// ResourceApiKey
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create an api key
|
||||
// - "ActionDelete" :: delete an api key
|
||||
// - "ActionRead" :: read api key details (secrets are not stored)
|
||||
// - "ActionUpdate" :: update an api key, eg expires
|
||||
ResourceApiKey = Object{
|
||||
Type: "api_key",
|
||||
}
|
||||
|
||||
// ResourceAssignOrgRole
|
||||
// Valid Actions
|
||||
// - "ActionAssign" :: ability to assign org scoped roles
|
||||
// - "ActionDelete" :: ability to delete org scoped roles
|
||||
// - "ActionRead" :: view what roles are assignable
|
||||
ResourceAssignOrgRole = Object{
|
||||
Type: "assign_org_role",
|
||||
}
|
||||
|
||||
// ResourceAssignRole
|
||||
// Valid Actions
|
||||
// - "ActionAssign" :: ability to assign roles
|
||||
// - "ActionDelete" :: ability to delete roles
|
||||
// - "ActionRead" :: view what roles are assignable
|
||||
ResourceAssignRole = Object{
|
||||
Type: "assign_role",
|
||||
}
|
||||
|
||||
// ResourceAuditLog
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create new audit log entries
|
||||
// - "ActionRead" :: read audit logs
|
||||
ResourceAuditLog = Object{
|
||||
Type: "audit_log",
|
||||
}
|
||||
|
||||
// ResourceDebugInfo
|
||||
// Valid Actions
|
||||
// - "ActionRead" :: access to debug routes
|
||||
ResourceDebugInfo = Object{
|
||||
Type: "debug_info",
|
||||
}
|
||||
|
||||
// ResourceDeploymentConfig
|
||||
// Valid Actions
|
||||
// - "ActionRead" :: read deployment config
|
||||
// - "ActionUpdate" :: updating health information
|
||||
ResourceDeploymentConfig = Object{
|
||||
Type: "deployment_config",
|
||||
}
|
||||
|
||||
// ResourceDeploymentStats
|
||||
// Valid Actions
|
||||
// - "ActionRead" :: read deployment stats
|
||||
ResourceDeploymentStats = Object{
|
||||
Type: "deployment_stats",
|
||||
}
|
||||
|
||||
// ResourceFile
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a file
|
||||
// - "ActionRead" :: read files
|
||||
ResourceFile = Object{
|
||||
Type: "file",
|
||||
}
|
||||
|
||||
// ResourceGroup
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a group
|
||||
// - "ActionDelete" :: delete a group
|
||||
// - "ActionRead" :: read groups
|
||||
// - "ActionUpdate" :: update a group
|
||||
ResourceGroup = Object{
|
||||
Type: "group",
|
||||
}
|
||||
|
||||
// ResourceLicense
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a license
|
||||
// - "ActionDelete" :: delete license
|
||||
// - "ActionRead" :: read licenses
|
||||
ResourceLicense = Object{
|
||||
Type: "license",
|
||||
}
|
||||
|
||||
// ResourceOauth2App
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: make an OAuth2 app.
|
||||
// - "ActionDelete" :: delete an OAuth2 app
|
||||
// - "ActionRead" :: read OAuth2 apps
|
||||
// - "ActionUpdate" :: update the properties of the OAuth2 app.
|
||||
ResourceOauth2App = Object{
|
||||
Type: "oauth2_app",
|
||||
}
|
||||
|
||||
// ResourceOauth2AppCodeToken
|
||||
// Valid Actions
|
||||
// - "ActionCreate" ::
|
||||
// - "ActionDelete" ::
|
||||
// - "ActionRead" ::
|
||||
ResourceOauth2AppCodeToken = Object{
|
||||
Type: "oauth2_app_code_token",
|
||||
}
|
||||
|
||||
// ResourceOauth2AppSecret
|
||||
// Valid Actions
|
||||
// - "ActionCreate" ::
|
||||
// - "ActionDelete" ::
|
||||
// - "ActionRead" ::
|
||||
// - "ActionUpdate" ::
|
||||
ResourceOauth2AppSecret = Object{
|
||||
Type: "oauth2_app_secret",
|
||||
}
|
||||
|
||||
// ResourceOrganization
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create an organization
|
||||
// - "ActionDelete" :: delete an organization
|
||||
// - "ActionRead" :: read organizations
|
||||
// - "ActionUpdate" :: update an organization
|
||||
ResourceOrganization = Object{
|
||||
Type: "organization",
|
||||
}
|
||||
|
||||
// ResourceOrganizationMember
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create an organization member
|
||||
// - "ActionDelete" :: delete member
|
||||
// - "ActionRead" :: read member
|
||||
// - "ActionUpdate" :: update an organization member
|
||||
ResourceOrganizationMember = Object{
|
||||
Type: "organization_member",
|
||||
}
|
||||
|
||||
// ResourceProvisionerDaemon
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a provisioner daemon
|
||||
// - "ActionDelete" :: delete a provisioner daemon
|
||||
// - "ActionRead" :: read provisioner daemon
|
||||
// - "ActionUpdate" :: update a provisioner daemon
|
||||
ResourceProvisionerDaemon = Object{
|
||||
Type: "provisioner_daemon",
|
||||
}
|
||||
|
||||
// ResourceReplicas
|
||||
// Valid Actions
|
||||
// - "ActionRead" :: read replicas
|
||||
ResourceReplicas = Object{
|
||||
Type: "replicas",
|
||||
}
|
||||
|
||||
// ResourceSystem
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create system resources
|
||||
// - "ActionDelete" :: delete system resources
|
||||
// - "ActionRead" :: view system resources
|
||||
// - "ActionUpdate" :: update system resources
|
||||
ResourceSystem = Object{
|
||||
Type: "system",
|
||||
}
|
||||
|
||||
// ResourceTailnetCoordinator
|
||||
// Valid Actions
|
||||
// - "ActionCreate" ::
|
||||
// - "ActionDelete" ::
|
||||
// - "ActionRead" ::
|
||||
// - "ActionUpdate" ::
|
||||
ResourceTailnetCoordinator = Object{
|
||||
Type: "tailnet_coordinator",
|
||||
}
|
||||
|
||||
// ResourceTemplate
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a template
|
||||
// - "ActionDelete" :: delete a template
|
||||
// - "ActionRead" :: read template
|
||||
// - "ActionUpdate" :: update a template
|
||||
// - "ActionViewInsights" :: view insights
|
||||
ResourceTemplate = Object{
|
||||
Type: "template",
|
||||
}
|
||||
|
||||
// ResourceUser
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a new user
|
||||
// - "ActionDelete" :: delete an existing user
|
||||
// - "ActionRead" :: read user data
|
||||
// - "ActionReadPersonal" :: read personal user data like user settings and auth links
|
||||
// - "ActionUpdate" :: update an existing user
|
||||
// - "ActionUpdatePersonal" :: update personal data
|
||||
ResourceUser = Object{
|
||||
Type: "user",
|
||||
}
|
||||
|
||||
// ResourceWorkspace
|
||||
// Valid Actions
|
||||
// - "ActionApplicationConnect" :: connect to workspace apps via browser
|
||||
// - "ActionCreate" :: create a new workspace
|
||||
// - "ActionDelete" :: delete workspace
|
||||
// - "ActionRead" :: read workspace data to view on the UI
|
||||
// - "ActionSSH" :: ssh into a given workspace
|
||||
// - "ActionWorkspaceStart" :: allows starting a workspace
|
||||
// - "ActionWorkspaceStop" :: allows stopping a workspace
|
||||
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
|
||||
ResourceWorkspace = Object{
|
||||
Type: "workspace",
|
||||
}
|
||||
|
||||
// ResourceWorkspaceDormant
|
||||
// Valid Actions
|
||||
// - "ActionApplicationConnect" :: connect to workspace apps via browser
|
||||
// - "ActionCreate" :: create a new workspace
|
||||
// - "ActionDelete" :: delete workspace
|
||||
// - "ActionRead" :: read workspace data to view on the UI
|
||||
// - "ActionSSH" :: ssh into a given workspace
|
||||
// - "ActionWorkspaceStart" :: allows starting a workspace
|
||||
// - "ActionWorkspaceStop" :: allows stopping a workspace
|
||||
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
|
||||
ResourceWorkspaceDormant = Object{
|
||||
Type: "workspace_dormant",
|
||||
}
|
||||
|
||||
// ResourceWorkspaceProxy
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a workspace proxy
|
||||
// - "ActionDelete" :: delete a workspace proxy
|
||||
// - "ActionRead" :: read and use a workspace proxy
|
||||
// - "ActionUpdate" :: update a workspace proxy
|
||||
ResourceWorkspaceProxy = Object{
|
||||
Type: "workspace_proxy",
|
||||
}
|
||||
)
|
||||
|
||||
func AllResources() []Objecter {
|
||||
return []Objecter{
|
||||
ResourceWildcard,
|
||||
ResourceApiKey,
|
||||
ResourceAssignOrgRole,
|
||||
ResourceAssignRole,
|
||||
ResourceAuditLog,
|
||||
ResourceDebugInfo,
|
||||
ResourceDeploymentConfig,
|
||||
ResourceDeploymentStats,
|
||||
ResourceDeploymentValues,
|
||||
ResourceFile,
|
||||
ResourceGroup,
|
||||
ResourceLicense,
|
||||
ResourceOAuth2ProviderApp,
|
||||
ResourceOAuth2ProviderAppCodeToken,
|
||||
ResourceOAuth2ProviderAppSecret,
|
||||
ResourceOrgRoleAssignment,
|
||||
ResourceOauth2App,
|
||||
ResourceOauth2AppCodeToken,
|
||||
ResourceOauth2AppSecret,
|
||||
ResourceOrganization,
|
||||
ResourceOrganizationMember,
|
||||
ResourceProvisionerDaemon,
|
||||
ResourceReplicas,
|
||||
ResourceRoleAssignment,
|
||||
ResourceSystem,
|
||||
ResourceTailnetCoordinator,
|
||||
ResourceTemplate,
|
||||
ResourceTemplateInsights,
|
||||
ResourceUser,
|
||||
ResourceUserData,
|
||||
ResourceUserWorkspaceBuildParameters,
|
||||
ResourceWildcard,
|
||||
ResourceWorkspace,
|
||||
ResourceWorkspaceApplicationConnect,
|
||||
ResourceWorkspaceBuild,
|
||||
ResourceWorkspaceDormant,
|
||||
ResourceWorkspaceExecution,
|
||||
ResourceWorkspaceProxy,
|
||||
}
|
||||
}
|
||||
|
||||
func AllActions() []policy.Action {
|
||||
return []policy.Action{
|
||||
policy.ActionApplicationConnect,
|
||||
policy.ActionAssign,
|
||||
policy.ActionCreate,
|
||||
policy.ActionDelete,
|
||||
policy.ActionRead,
|
||||
policy.ActionReadPersonal,
|
||||
policy.ActionSSH,
|
||||
policy.ActionUpdate,
|
||||
policy.ActionUpdatePersonal,
|
||||
policy.ActionUse,
|
||||
policy.ActionViewInsights,
|
||||
policy.ActionWorkspaceStart,
|
||||
policy.ActionWorkspaceStop,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,14 +184,14 @@ func TestAllResources(t *testing.T) {
|
||||
var typeNames []string
|
||||
resources := rbac.AllResources()
|
||||
for _, r := range resources {
|
||||
if r.Type == "" {
|
||||
t.Errorf("empty type name: %s", r.Type)
|
||||
if r.RBACObject().Type == "" {
|
||||
t.Errorf("empty type name: %s", r.RBACObject().Type)
|
||||
continue
|
||||
}
|
||||
if slice.Contains(typeNames, r.Type) {
|
||||
t.Errorf("duplicate type name: %s", r.Type)
|
||||
if slice.Contains(typeNames, r.RBACObject().Type) {
|
||||
t.Errorf("duplicate type name: %s", r.RBACObject().Type)
|
||||
continue
|
||||
}
|
||||
typeNames = append(typeNames, r.Type)
|
||||
typeNames = append(typeNames, r.RBACObject().Type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package policy
|
||||
|
||||
const WildcardSymbol = "*"
|
||||
|
||||
// Action represents the allowed actions to be done on an object.
|
||||
type Action string
|
||||
|
||||
@@ -8,4 +10,236 @@ const (
|
||||
ActionRead Action = "read"
|
||||
ActionUpdate Action = "update"
|
||||
ActionDelete Action = "delete"
|
||||
|
||||
ActionUse Action = "use"
|
||||
ActionSSH Action = "ssh"
|
||||
ActionApplicationConnect Action = "application_connect"
|
||||
ActionViewInsights Action = "view_insights"
|
||||
|
||||
ActionWorkspaceStart Action = "start"
|
||||
ActionWorkspaceStop Action = "stop"
|
||||
|
||||
ActionAssign Action = "assign"
|
||||
|
||||
ActionReadPersonal Action = "read_personal"
|
||||
ActionUpdatePersonal Action = "update_personal"
|
||||
)
|
||||
|
||||
type PermissionDefinition struct {
|
||||
// name is optional. Used to override "Type" for function naming.
|
||||
Name string
|
||||
// Actions are a map of actions to some description of what the action
|
||||
// should represent. The key in the actions map is the verb to use
|
||||
// in the rbac policy.
|
||||
Actions map[Action]ActionDefinition
|
||||
}
|
||||
|
||||
type ActionDefinition struct {
|
||||
// Human friendly description to explain the action.
|
||||
Description string
|
||||
}
|
||||
|
||||
func actDef(description string) ActionDefinition {
|
||||
return ActionDefinition{
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
var workspaceActions = map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create a new workspace"),
|
||||
ActionRead: actDef("read workspace data to view on the UI"),
|
||||
// TODO: Make updates more granular
|
||||
ActionUpdate: actDef("edit workspace settings (scheduling, permissions, parameters)"),
|
||||
ActionDelete: actDef("delete workspace"),
|
||||
|
||||
// Workspace provisioning. Start & stop are different so dormant workspaces can be
|
||||
// stopped, but not stared.
|
||||
ActionWorkspaceStart: actDef("allows starting a workspace"),
|
||||
ActionWorkspaceStop: actDef("allows stopping a workspace"),
|
||||
|
||||
// Running a workspace
|
||||
ActionSSH: actDef("ssh into a given workspace"),
|
||||
ActionApplicationConnect: actDef("connect to workspace apps via browser"),
|
||||
}
|
||||
|
||||
// RBACPermissions is indexed by the type
|
||||
var RBACPermissions = map[string]PermissionDefinition{
|
||||
// Wildcard is every object, and the action "*" provides all actions.
|
||||
// So can grant all actions on all types.
|
||||
WildcardSymbol: {
|
||||
Name: "Wildcard",
|
||||
Actions: map[Action]ActionDefinition{},
|
||||
},
|
||||
"user": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
// Actions deal with site wide user objects.
|
||||
ActionRead: actDef("read user data"),
|
||||
ActionCreate: actDef("create a new user"),
|
||||
ActionUpdate: actDef("update an existing user"),
|
||||
ActionDelete: actDef("delete an existing user"),
|
||||
|
||||
ActionReadPersonal: actDef("read personal user data like user settings and auth links"),
|
||||
ActionUpdatePersonal: actDef("update personal data"),
|
||||
},
|
||||
},
|
||||
"workspace": {
|
||||
Actions: workspaceActions,
|
||||
},
|
||||
// Dormant workspaces have the same perms as workspaces.
|
||||
"workspace_dormant": {
|
||||
Actions: workspaceActions,
|
||||
},
|
||||
"workspace_proxy": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create a workspace proxy"),
|
||||
ActionDelete: actDef("delete a workspace proxy"),
|
||||
ActionUpdate: actDef("update a workspace proxy"),
|
||||
ActionRead: actDef("read and use a workspace proxy"),
|
||||
},
|
||||
},
|
||||
"license": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create a license"),
|
||||
ActionRead: actDef("read licenses"),
|
||||
ActionDelete: actDef("delete license"),
|
||||
// Licenses are immutable, so update makes no sense
|
||||
},
|
||||
},
|
||||
"audit_log": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: actDef("read audit logs"),
|
||||
ActionCreate: actDef("create new audit log entries"),
|
||||
},
|
||||
},
|
||||
"deployment_config": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: actDef("read deployment config"),
|
||||
ActionUpdate: actDef("updating health information"),
|
||||
},
|
||||
},
|
||||
"deployment_stats": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: actDef("read deployment stats"),
|
||||
},
|
||||
},
|
||||
"replicas": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: actDef("read replicas"),
|
||||
},
|
||||
},
|
||||
"template": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create a template"),
|
||||
// TODO: Create a use permission maybe?
|
||||
ActionRead: actDef("read template"),
|
||||
ActionUpdate: actDef("update a template"),
|
||||
ActionDelete: actDef("delete a template"),
|
||||
ActionViewInsights: actDef("view insights"),
|
||||
},
|
||||
},
|
||||
"group": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create a group"),
|
||||
ActionRead: actDef("read groups"),
|
||||
ActionDelete: actDef("delete a group"),
|
||||
ActionUpdate: actDef("update a group"),
|
||||
},
|
||||
},
|
||||
"file": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create a file"),
|
||||
ActionRead: actDef("read files"),
|
||||
},
|
||||
},
|
||||
"provisioner_daemon": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create a provisioner daemon"),
|
||||
// TODO: Move to use?
|
||||
ActionRead: actDef("read provisioner daemon"),
|
||||
ActionUpdate: actDef("update a provisioner daemon"),
|
||||
ActionDelete: actDef("delete a provisioner daemon"),
|
||||
},
|
||||
},
|
||||
"organization": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create an organization"),
|
||||
ActionRead: actDef("read organizations"),
|
||||
ActionUpdate: actDef("update an organization"),
|
||||
ActionDelete: actDef("delete an organization"),
|
||||
},
|
||||
},
|
||||
"organization_member": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create an organization member"),
|
||||
ActionRead: actDef("read member"),
|
||||
ActionUpdate: actDef("update an organization member"),
|
||||
ActionDelete: actDef("delete member"),
|
||||
},
|
||||
},
|
||||
"debug_info": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: actDef("access to debug routes"),
|
||||
},
|
||||
},
|
||||
"system": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create system resources"),
|
||||
ActionRead: actDef("view system resources"),
|
||||
ActionUpdate: actDef("update system resources"),
|
||||
ActionDelete: actDef("delete system resources"),
|
||||
},
|
||||
},
|
||||
"api_key": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("create an api key"),
|
||||
ActionRead: actDef("read api key details (secrets are not stored)"),
|
||||
ActionDelete: actDef("delete an api key"),
|
||||
ActionUpdate: actDef("update an api key, eg expires"),
|
||||
},
|
||||
},
|
||||
"tailnet_coordinator": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef(""),
|
||||
ActionRead: actDef(""),
|
||||
ActionUpdate: actDef(""),
|
||||
ActionDelete: actDef(""),
|
||||
},
|
||||
},
|
||||
"assign_role": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionAssign: actDef("ability to assign roles"),
|
||||
ActionRead: actDef("view what roles are assignable"),
|
||||
ActionDelete: actDef("ability to delete roles"),
|
||||
},
|
||||
},
|
||||
"assign_org_role": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionAssign: actDef("ability to assign org scoped roles"),
|
||||
ActionRead: actDef("view what roles are assignable"),
|
||||
ActionDelete: actDef("ability to delete org scoped roles"),
|
||||
},
|
||||
},
|
||||
"oauth2_app": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef("make an OAuth2 app."),
|
||||
ActionRead: actDef("read OAuth2 apps"),
|
||||
ActionUpdate: actDef("update the properties of the OAuth2 app."),
|
||||
ActionDelete: actDef("delete an OAuth2 app"),
|
||||
},
|
||||
},
|
||||
"oauth2_app_secret": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef(""),
|
||||
ActionRead: actDef(""),
|
||||
ActionUpdate: actDef(""),
|
||||
ActionDelete: actDef(""),
|
||||
},
|
||||
},
|
||||
"oauth2_app_code_token": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: actDef(""),
|
||||
ActionRead: actDef(""),
|
||||
ActionDelete: actDef(""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+49
-36
@@ -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
@@ -34,10 +34,10 @@ func TestOwnerExec(t *testing.T) {
|
||||
})
|
||||
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
|
||||
|
||||
auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
|
||||
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
// Exec a random workspace
|
||||
err := auth.Authorize(context.Background(), owner, policy.ActionCreate,
|
||||
rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
|
||||
err := auth.Authorize(context.Background(), owner, policy.ActionSSH,
|
||||
rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
|
||||
require.ErrorAsf(t, err, &rbac.UnauthorizedError{}, "expected unauthorized error")
|
||||
})
|
||||
|
||||
@@ -47,20 +47,22 @@ func TestOwnerExec(t *testing.T) {
|
||||
})
|
||||
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
|
||||
|
||||
auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
|
||||
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
// Exec a random workspace
|
||||
err := auth.Authorize(context.Background(), owner, policy.ActionCreate,
|
||||
rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
|
||||
err := auth.Authorize(context.Background(), owner, policy.ActionSSH,
|
||||
rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
|
||||
require.NoError(t, err, "expected owner can")
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: add the SYSTEM to the MATRIX
|
||||
// nolint:tparallel,paralleltest -- subtests share a map, just run sequentially.
|
||||
func TestRolePermissions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
|
||||
crud := []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
|
||||
|
||||
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
// currentUser is anything that references "me", "mine", or "my".
|
||||
currentUser := uuid.New()
|
||||
@@ -145,8 +147,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
{
|
||||
Name: "MyWorkspaceInOrgExecution",
|
||||
// When creating the WithID won't be set, but it does not change the result.
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceWorkspaceExecution.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
Actions: []policy.Action{policy.ActionSSH},
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgMemberMe},
|
||||
false: {orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
@@ -155,16 +157,16 @@ func TestRolePermissions(t *testing.T) {
|
||||
{
|
||||
Name: "MyWorkspaceInOrgAppConnect",
|
||||
// When creating the WithID won't be set, but it does not change the result.
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceWorkspaceApplicationConnect.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
Actions: []policy.Action{policy.ActionApplicationConnect},
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, orgMemberMe},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
true: {owner, orgMemberMe},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin, orgAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Templates",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights},
|
||||
Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, templateAdmin},
|
||||
@@ -191,7 +193,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "MyFile",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead},
|
||||
Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, memberMe, orgMemberMe, templateAdmin},
|
||||
@@ -227,8 +229,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "RoleAssignment",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceRoleAssignment,
|
||||
Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete},
|
||||
Resource: rbac.ResourceAssignRole,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, userAdmin},
|
||||
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin},
|
||||
@@ -237,7 +239,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
{
|
||||
Name: "ReadRoleAssignment",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceRoleAssignment,
|
||||
Resource: rbac.ResourceAssignRole,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
@@ -245,8 +247,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "OrgRoleAssignment",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
|
||||
Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete},
|
||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin},
|
||||
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
@@ -255,7 +257,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
{
|
||||
Name: "ReadOrgRoleAssignment",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
|
||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, orgMemberMe},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
@@ -263,8 +265,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "APIKey",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceAPIKey.WithID(apiKeyID).WithOwner(currentUser.String()),
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgMemberMe, memberMe},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
@@ -272,8 +274,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "UserData",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceUserData.WithID(currentUser).WithOwner(currentUser.String()),
|
||||
Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal},
|
||||
Resource: rbac.ResourceUserObject(currentUser),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgMemberMe, memberMe, userAdmin},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin},
|
||||
@@ -312,6 +314,15 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "Groups",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionDelete, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, userAdmin},
|
||||
false: {memberMe, otherOrgAdmin, orgMemberMe, otherOrgMember, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GroupsRead",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
@@ -321,7 +332,16 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceDormant",
|
||||
Actions: rbac.AllActions(),
|
||||
Actions: append(crud, policy.ActionWorkspaceStop),
|
||||
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {orgMemberMe, orgAdmin, owner},
|
||||
false: {userAdmin, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceDormantUse",
|
||||
Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionApplicationConnect, policy.ActionSSH},
|
||||
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {},
|
||||
@@ -330,25 +350,198 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceBuild",
|
||||
Actions: rbac.AllActions(),
|
||||
Resource: rbac.ResourceWorkspaceBuild.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionWorkspaceStop},
|
||||
Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, orgMemberMe},
|
||||
false: {userAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, memberMe},
|
||||
},
|
||||
},
|
||||
// Some admin style resources
|
||||
{
|
||||
Name: "Licences",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
|
||||
Resource: rbac.ResourceLicense,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DeploymentStats",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceDeploymentStats,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DeploymentConfig",
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceDeploymentConfig,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DebugInfo",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceDebugInfo,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Replicas",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceReplicas,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TailnetCoordinator",
|
||||
Actions: crud,
|
||||
Resource: rbac.ResourceTailnetCoordinator,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "AuditLogs",
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionCreate},
|
||||
Resource: rbac.ResourceAuditLog,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ProvisionerDaemons",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, templateAdmin, orgAdmin},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ProvisionerDaemonsRead",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
// This should be fixed when multi-org goes live
|
||||
true: {owner, templateAdmin, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UserProvisionerDaemons",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, templateAdmin, orgMemberMe, orgAdmin},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "System",
|
||||
Actions: crud,
|
||||
Resource: rbac.ResourceSystem,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Oauth2App",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceOauth2App,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Oauth2AppRead",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceOauth2App,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Oauth2AppSecret",
|
||||
Actions: crud,
|
||||
Resource: rbac.ResourceOauth2AppSecret,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Oauth2Token",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
|
||||
Resource: rbac.ResourceOauth2AppCodeToken,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceProxy",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceWorkspaceProxy,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceProxyRead",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceWorkspaceProxy,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// We expect every permission to be tested above.
|
||||
remainingPermissions := make(map[string]map[policy.Action]bool)
|
||||
for rtype, perms := range policy.RBACPermissions {
|
||||
remainingPermissions[rtype] = make(map[policy.Action]bool)
|
||||
for action := range perms.Actions {
|
||||
remainingPermissions[rtype][action] = true
|
||||
}
|
||||
}
|
||||
|
||||
passed := true
|
||||
// nolint:tparallel,paralleltest
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
// nolint:tparallel,paralleltest -- These share the same remainingPermissions map
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
remainingSubjs := make(map[string]struct{})
|
||||
for _, subj := range requiredSubjects {
|
||||
remainingSubjs[subj.Name] = struct{}{}
|
||||
}
|
||||
|
||||
for _, action := range c.Actions {
|
||||
err := c.Resource.ValidAction(action)
|
||||
ok := assert.NoError(t, err, "%q is not a valid action for type %q", action, c.Resource.Type)
|
||||
if !ok {
|
||||
passed = passed && assert.NoError(t, err, "%q is not a valid action for type %q", action, c.Resource.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
for result, subjs := range c.AuthorizeMap {
|
||||
for _, subj := range subjs {
|
||||
delete(remainingSubjs, subj.Name)
|
||||
@@ -359,11 +552,13 @@ func TestRolePermissions(t *testing.T) {
|
||||
if actor.Scope == nil {
|
||||
actor.Scope = rbac.ScopeAll
|
||||
}
|
||||
|
||||
delete(remainingPermissions[c.Resource.Type], action)
|
||||
err := auth.Authorize(context.Background(), actor, action, c.Resource)
|
||||
if result {
|
||||
assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
|
||||
passed = passed && assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
|
||||
} else {
|
||||
assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg))
|
||||
passed = passed && assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,6 +566,18 @@ func TestRolePermissions(t *testing.T) {
|
||||
require.Empty(t, remainingSubjs, "test should cover all subjects")
|
||||
})
|
||||
}
|
||||
|
||||
// Only run these if the tests on top passed. Otherwise, the error output is too noisy.
|
||||
if passed {
|
||||
for rtype, v := range remainingPermissions {
|
||||
// nolint:tparallel,paralleltest -- Making a subtest for easier diagnosing failures.
|
||||
t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) {
|
||||
if len(v) > 0 {
|
||||
assert.Equal(t, map[policy.Action]bool{}, v, "remaining permissions should be empty for type %q", rtype)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOrgRole(t *testing.T) {
|
||||
|
||||
@@ -61,12 +61,12 @@ var builtinScopes = map[ScopeName]Scope{
|
||||
Name: fmt.Sprintf("Scope_%s", ScopeAll),
|
||||
DisplayName: "All operations",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceWildcard.Type: {WildcardSymbol},
|
||||
ResourceWildcard.Type: {policy.WildcardSymbol},
|
||||
}),
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
AllowIDList: []string{WildcardSymbol},
|
||||
AllowIDList: []string{policy.WildcardSymbol},
|
||||
},
|
||||
|
||||
ScopeApplicationConnect: {
|
||||
@@ -74,12 +74,12 @@ var builtinScopes = map[ScopeName]Scope{
|
||||
Name: fmt.Sprintf("Scope_%s", ScopeApplicationConnect),
|
||||
DisplayName: "Ability to connect to applications",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceWorkspaceApplicationConnect.Type: {policy.ActionCreate},
|
||||
ResourceWorkspace.Type: {policy.ActionApplicationConnect},
|
||||
}),
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
AllowIDList: []string{WildcardSymbol},
|
||||
AllowIDList: []string{policy.WildcardSymbol},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user