mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat: Add user roles, but do not yet enforce them (#1200)
* chore: Rework roles to be expandable by name alone
This commit is contained in:
@ -38,6 +38,23 @@ type authSubject struct {
|
||||
Roles []Role `json:"roles"`
|
||||
}
|
||||
|
||||
// AuthorizeByRoleName will expand all roleNames into roles before calling Authorize().
|
||||
// This is the function intended to be used outside this package.
|
||||
// The role is fetched from the builtin map located in memory.
|
||||
func (a RegoAuthorizer) AuthorizeByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error {
|
||||
roles := make([]Role, 0, len(roleNames))
|
||||
for _, n := range roleNames {
|
||||
r, err := RoleByName(n)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get role permissions: %w", err)
|
||||
}
|
||||
roles = append(roles, r)
|
||||
}
|
||||
return a.Authorize(ctx, subjectID, roles, action, object)
|
||||
}
|
||||
|
||||
// Authorize allows passing in custom Roles.
|
||||
// This is really helpful for unit testing, as we can create custom roles to exercise edge cases.
|
||||
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, action Action, object Object) error {
|
||||
input := map[string]interface{}{
|
||||
"subject": authSubject{
|
||||
|
@ -3,8 +3,11 @@ package rbac_test
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -14,19 +17,26 @@ import (
|
||||
|
||||
// subject is required because rego needs
|
||||
type subject struct {
|
||||
UserID string `json:"id"`
|
||||
Roles []rbac.Role `json:"roles"`
|
||||
UserID string `json:"id"`
|
||||
// For the unit test we want to pass in the roles directly, instead of just
|
||||
// by name. This allows us to test custom roles that do not exist in the product,
|
||||
// but test edge cases of the implementation.
|
||||
Roles []rbac.Role `json:"roles"`
|
||||
}
|
||||
|
||||
// TestAuthorizeDomain test the very basic roles that are commonly used.
|
||||
func TestAuthorizeDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
defOrg := "default"
|
||||
defOrg := uuid.New()
|
||||
unuseID := uuid.New()
|
||||
wrkID := "1234"
|
||||
|
||||
user := subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{rbac.RoleMember, rbac.RoleOrgMember(defOrg)},
|
||||
Roles: []rbac.Role{
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
must(rbac.RoleByName(rbac.RoleOrgMember(defOrg))),
|
||||
},
|
||||
}
|
||||
|
||||
testAuthorize(t, "Member", user, []authTestCase{
|
||||
@ -44,10 +54,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false},
|
||||
@ -57,10 +67,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
@ -99,10 +109,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false},
|
||||
@ -112,10 +122,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
@ -126,8 +136,8 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
user = subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleOrgAdmin(defOrg),
|
||||
rbac.RoleMember,
|
||||
must(rbac.RoleByName(rbac.RoleOrgAdmin(defOrg))),
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
},
|
||||
}
|
||||
|
||||
@ -146,10 +156,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true},
|
||||
@ -159,10 +169,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
@ -173,8 +183,8 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
user = subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleAdmin,
|
||||
rbac.RoleMember,
|
||||
must(rbac.RoleByName(rbac.RoleAdmin())),
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
},
|
||||
}
|
||||
|
||||
@ -193,10 +203,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: true},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: true},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true},
|
||||
@ -206,10 +216,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: true},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
@ -221,7 +231,19 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
user = subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleWorkspaceAgent(wrkID),
|
||||
{
|
||||
Name: fmt.Sprintf("agent-%s", wrkID),
|
||||
// This is at the site level to prevent the token from losing access if the user
|
||||
// is kicked from the org
|
||||
Site: []rbac.Permission{
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: rbac.ResourceWorkspace.Type,
|
||||
ResourceID: wrkID,
|
||||
Action: rbac.ActionRead,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -245,10 +267,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true},
|
||||
@ -258,10 +280,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
@ -288,10 +310,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All()},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)},
|
||||
@ -301,10 +323,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
@ -321,7 +343,7 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
Name: "ReadOnlyOrgAndUser",
|
||||
Site: []rbac.Permission{},
|
||||
Org: map[string][]rbac.Permission{
|
||||
defOrg: {{
|
||||
defOrg.String(): {{
|
||||
Negate: false,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
@ -360,10 +382,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true},
|
||||
@ -373,10 +395,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
@ -405,10 +427,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All()},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)},
|
||||
@ -418,10 +440,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
@ -433,14 +455,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
// TestAuthorizeLevels ensures level overrides are acting appropriately
|
||||
//nolint:paralleltest
|
||||
func TestAuthorizeLevels(t *testing.T) {
|
||||
defOrg := "default"
|
||||
defOrg := uuid.New()
|
||||
unusedID := uuid.New()
|
||||
wrkID := "1234"
|
||||
|
||||
user := subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleAdmin,
|
||||
rbac.RoleOrgDenyAll(defOrg),
|
||||
must(rbac.RoleByName(rbac.RoleAdmin())),
|
||||
{
|
||||
Name: "org-deny:" + defOrg.String(),
|
||||
Org: map[string][]rbac.Permission{
|
||||
defOrg.String(): {
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-deny-all",
|
||||
// List out deny permissions explicitly
|
||||
@ -476,10 +511,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All()},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID)},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)},
|
||||
@ -489,10 +524,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID)},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
@ -514,7 +549,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
rbac.RoleOrgAdmin(defOrg),
|
||||
must(rbac.RoleByName(rbac.RoleOrgAdmin(defOrg))),
|
||||
{
|
||||
Name: "user-deny-all",
|
||||
// List out deny permissions explicitly
|
||||
@ -549,10 +584,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.All(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true},
|
||||
@ -562,10 +597,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
190
coderd/rbac/builtin.go
Normal file
190
coderd/rbac/builtin.go
Normal file
@ -0,0 +1,190 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
admin string = "admin"
|
||||
member string = "member"
|
||||
auditor string = "auditor"
|
||||
|
||||
orgAdmin string = "organization-admin"
|
||||
orgMember string = "organization-member"
|
||||
)
|
||||
|
||||
// The functions below ONLY need to exist for roles that are "defaulted" in some way.
|
||||
// Any other roles (like auditor), can be listed and let the user select/assigned.
|
||||
// Once we have a database implementation, the "default" roles can be defined on the
|
||||
// site and orgs, and these functions can be removed.
|
||||
|
||||
func RoleAdmin() string {
|
||||
return roleName(admin, "")
|
||||
}
|
||||
|
||||
func RoleMember() string {
|
||||
return roleName(member, "")
|
||||
}
|
||||
|
||||
func RoleOrgAdmin(organizationID uuid.UUID) string {
|
||||
return roleName(orgAdmin, organizationID.String())
|
||||
}
|
||||
|
||||
func RoleOrgMember(organizationID uuid.UUID) string {
|
||||
return roleName(orgMember, organizationID.String())
|
||||
}
|
||||
|
||||
var (
|
||||
// builtInRoles are just a hard coded set for now. Ideally we store these in
|
||||
// the database. Right now they are functions because the org id should scope
|
||||
// certain roles. When we store them in the database, each organization should
|
||||
// create the roles that are assignable in the org. This isn't a hard problem to solve,
|
||||
// it's just easier as a function right now.
|
||||
//
|
||||
// This map will be replaced by database storage defined by this ticket.
|
||||
// https://github.com/coder/coder/issues/1194
|
||||
builtInRoles = map[string]func(orgID string) Role{
|
||||
// admin grants all actions to all resources.
|
||||
admin: func(_ string) Role {
|
||||
return Role{
|
||||
Name: admin,
|
||||
Site: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
// member grants all actions to all resources owned by the user
|
||||
member: func(_ string) Role {
|
||||
return Role{
|
||||
Name: member,
|
||||
User: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
// auditor provides all permissions required to effectively read and understand
|
||||
// audit log events.
|
||||
// TODO: Finish the auditor as we add resources.
|
||||
auditor: func(_ string) Role {
|
||||
return Role{
|
||||
Name: "auditor",
|
||||
Site: permissions(map[Object][]Action{
|
||||
// Should be able to read all template details, even in orgs they
|
||||
// are not in.
|
||||
ResourceTemplate: {ActionRead},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
// orgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
orgAdmin: func(organizationID string) Role {
|
||||
return Role{
|
||||
Name: roleName(orgAdmin, organizationID),
|
||||
Org: map[string][]Permission{
|
||||
organizationID: {
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// orgMember has an empty set of permissions, this just implies their membership
|
||||
// in an organization.
|
||||
orgMember: func(organizationID string) Role {
|
||||
return Role{
|
||||
Name: roleName(orgMember, organizationID),
|
||||
Org: map[string][]Permission{
|
||||
organizationID: {},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// RoleByName returns the permissions associated with a given role name.
|
||||
// This allows just the role names to be stored and expanded when required.
|
||||
func RoleByName(name string) (Role, error) {
|
||||
roleName, orgID, err := roleSplit(name)
|
||||
if err != nil {
|
||||
return Role{}, xerrors.Errorf(":%w", err)
|
||||
}
|
||||
|
||||
roleFunc, ok := builtInRoles[roleName]
|
||||
if !ok {
|
||||
// No role found
|
||||
return Role{}, xerrors.Errorf("role %q not found", roleName)
|
||||
}
|
||||
|
||||
// Ensure all org roles are properly scoped a non-empty organization id.
|
||||
// This is just some defensive programming.
|
||||
role := roleFunc(orgID)
|
||||
if len(role.Org) > 0 && orgID == "" {
|
||||
return Role{}, xerrors.Errorf("expect a org id for role %q", roleName)
|
||||
}
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func IsOrgRole(roleName string) (string, bool) {
|
||||
_, orgID, err := roleSplit(roleName)
|
||||
if err == nil && orgID != "" {
|
||||
return orgID, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// roleName is a quick helper function to return
|
||||
// role_name:scopeID
|
||||
// If no scopeID is required, only 'role_name' is returned
|
||||
func roleName(name string, orgID string) string {
|
||||
if orgID == "" {
|
||||
return name
|
||||
}
|
||||
return name + ":" + orgID
|
||||
}
|
||||
|
||||
func roleSplit(role string) (name string, orgID string, err error) {
|
||||
arr := strings.Split(role, ":")
|
||||
if len(arr) > 2 {
|
||||
return "", "", xerrors.Errorf("too many colons in role name")
|
||||
}
|
||||
|
||||
if arr[0] == "" {
|
||||
return "", "", xerrors.Errorf("role cannot be the empty string")
|
||||
}
|
||||
|
||||
if len(arr) == 2 {
|
||||
return arr[0], arr[1], nil
|
||||
}
|
||||
return arr[0], "", nil
|
||||
}
|
||||
|
||||
// permissions is just a helper function to make building roles that list out resources
|
||||
// and actions a bit easier.
|
||||
func permissions(perms map[Object][]Action) []Permission {
|
||||
list := make([]Permission, 0, len(perms))
|
||||
for k, actions := range perms {
|
||||
for _, act := range actions {
|
||||
act := act
|
||||
list = append(list, Permission{
|
||||
Negate: false,
|
||||
ResourceType: k.Type,
|
||||
ResourceID: WildcardSymbol,
|
||||
Action: act,
|
||||
})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
55
coderd/rbac/builtin_internal_test.go
Normal file
55
coderd/rbac/builtin_internal_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestRoleByName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("BuiltIns", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
Role Role
|
||||
}{
|
||||
{Role: builtInRoles[admin]("")},
|
||||
{Role: builtInRoles[member]("")},
|
||||
{Role: builtInRoles[auditor]("")},
|
||||
|
||||
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
||||
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
||||
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
||||
|
||||
{Role: builtInRoles[orgMember](uuid.New().String())},
|
||||
{Role: builtInRoles[orgMember](uuid.New().String())},
|
||||
{Role: builtInRoles[orgMember](uuid.New().String())},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.Role.Name, func(t *testing.T) {
|
||||
role, err := RoleByName(c.Role.Name)
|
||||
require.NoError(t, err, "role exists")
|
||||
require.Equal(t, c.Role, role)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// nolint:paralleltest
|
||||
t.Run("Errors", func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
_, err = RoleByName("")
|
||||
require.Error(t, err, "empty role")
|
||||
|
||||
_, err = RoleByName("too:many:colons")
|
||||
require.Error(t, err, "too many colons")
|
||||
|
||||
_, err = RoleByName(orgMember)
|
||||
require.Error(t, err, "expect orgID")
|
||||
})
|
||||
}
|
62
coderd/rbac/builtin_test.go
Normal file
62
coderd/rbac/builtin_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package rbac_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
)
|
||||
|
||||
func TestIsOrgRole(t *testing.T) {
|
||||
t.Parallel()
|
||||
randomUUID := uuid.New()
|
||||
|
||||
testCases := []struct {
|
||||
RoleName string
|
||||
OrgRole bool
|
||||
OrgID string
|
||||
}{
|
||||
// Not org roles
|
||||
{RoleName: rbac.RoleAdmin()},
|
||||
{RoleName: rbac.RoleMember()},
|
||||
{RoleName: "auditor"},
|
||||
|
||||
{
|
||||
RoleName: "a:bad:role",
|
||||
OrgRole: false,
|
||||
},
|
||||
{
|
||||
RoleName: "",
|
||||
OrgRole: false,
|
||||
},
|
||||
|
||||
// Org roles
|
||||
{
|
||||
RoleName: rbac.RoleOrgAdmin(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID.String(),
|
||||
},
|
||||
{
|
||||
RoleName: rbac.RoleOrgMember(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID.String(),
|
||||
},
|
||||
{
|
||||
RoleName: "test:example",
|
||||
OrgRole: true,
|
||||
OrgID: "example",
|
||||
},
|
||||
}
|
||||
|
||||
// nolint:paralleltest
|
||||
for _, c := range testCases {
|
||||
t.Run(c.RoleName, func(t *testing.T) {
|
||||
orgID, ok := rbac.IsOrgRole(c.RoleName)
|
||||
require.Equal(t, c.OrgRole, ok, "match expected org role")
|
||||
require.Equal(t, c.OrgID, orgID, "match expected org id")
|
||||
})
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
@ -16,14 +18,15 @@ func TestExample(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
authorizer, err := rbac.NewAuthorizer()
|
||||
require.NoError(t, err)
|
||||
defaultOrg := uuid.New()
|
||||
|
||||
// user will become an authn object, and can even be a database.User if it
|
||||
// fulfills the interface. Until then, use a placeholder.
|
||||
user := subject{
|
||||
UserID: "alice",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleOrgAdmin("default"),
|
||||
rbac.RoleMember,
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
must(rbac.RoleByName(rbac.RoleOrgAdmin(defaultOrg))),
|
||||
},
|
||||
}
|
||||
|
||||
@ -38,17 +41,24 @@ func TestExample(t *testing.T) {
|
||||
//nolint:paralleltest
|
||||
t.Run("ReadOrgWorkspaces", func(t *testing.T) {
|
||||
// To read all workspaces on the org 'default'
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default"))
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg(defaultOrg))
|
||||
require.NoError(t, err, "this user can read all org workspaces in 'default'")
|
||||
})
|
||||
|
||||
//nolint:paralleltest
|
||||
t.Run("ReadMyWorkspace", func(t *testing.T) {
|
||||
// Note 'database.Workspace' could fulfill the object interface and be passed in directly
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID))
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg(defaultOrg).WithOwner(user.UserID))
|
||||
require.NoError(t, err, "this user can their workspace")
|
||||
|
||||
err = authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"))
|
||||
err = authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg(defaultOrg).WithOwner(user.UserID).WithID("1234"))
|
||||
require.NoError(t, err, "this user can read workspace '1234'")
|
||||
})
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const WildcardSymbol = "*"
|
||||
|
||||
// Resources are just typed objects. Making resources this way allows directly
|
||||
@ -46,11 +50,11 @@ func (z Object) All() Object {
|
||||
}
|
||||
|
||||
// InOrg adds an org OwnerID to the resource
|
||||
func (z Object) InOrg(orgID string) Object {
|
||||
func (z Object) InOrg(orgID uuid.UUID) Object {
|
||||
return Object{
|
||||
ResourceID: z.ResourceID,
|
||||
Owner: z.Owner,
|
||||
OrgID: orgID,
|
||||
OrgID: orgID.String(),
|
||||
Type: z.Type,
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package rbac
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Permission is the format passed into the rego.
|
||||
type Permission struct {
|
||||
// Negate makes this a negative permission
|
||||
Negate bool `json:"negate"`
|
||||
@ -14,122 +13,15 @@ type Permission struct {
|
||||
// - Site level permissions apply EVERYWHERE
|
||||
// - Org level permissions apply to EVERYTHING in a given ORG
|
||||
// - User level permissions are the lowest
|
||||
// In most cases, you will just want to use the pre-defined roles
|
||||
// below.
|
||||
// This is the type passed into the rego as a json payload.
|
||||
// Users of this package should instead **only** use the role names, and
|
||||
// this package will expand the role names into their json payloads.
|
||||
type Role struct {
|
||||
Name string `json:"name"`
|
||||
Site []Permission `json:"site"`
|
||||
// Org is a map of orgid to permissions. We represent orgid as a string.
|
||||
// We scope the organizations in the role so we can easily combine all the
|
||||
// roles.
|
||||
Org map[string][]Permission `json:"org"`
|
||||
User []Permission `json:"user"`
|
||||
}
|
||||
|
||||
// Roles are stored as structs, so they can be serialized and stored. Until we store them elsewhere,
|
||||
// const's will do just fine.
|
||||
var (
|
||||
// RoleAdmin is a role that allows everything everywhere.
|
||||
RoleAdmin = Role{
|
||||
Name: "admin",
|
||||
Site: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
|
||||
// RoleMember is a role that allows access to user-level resources.
|
||||
RoleMember = Role{
|
||||
Name: "member",
|
||||
User: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
|
||||
// RoleAuditor is an example on how to give more precise permissions
|
||||
RoleAuditor = Role{
|
||||
Name: "auditor",
|
||||
Site: permissions(map[Object][]Action{
|
||||
//ResourceAuditLogs: {ActionRead},
|
||||
// Should be able to read user details to associate with logs.
|
||||
// Without this the user-id in logs is not very helpful
|
||||
ResourceWorkspace: {ActionRead},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func RoleOrgDenyAll(orgID string) Role {
|
||||
return Role{
|
||||
Name: "org-deny-" + orgID,
|
||||
Org: map[string][]Permission{
|
||||
orgID: {
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RoleOrgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
func RoleOrgAdmin(orgID string) Role {
|
||||
return Role{
|
||||
Name: "org-admin-" + orgID,
|
||||
Org: map[string][]Permission{
|
||||
orgID: {
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RoleOrgMember returns a role with default permissions in a given
|
||||
// organization scope.
|
||||
func RoleOrgMember(orgID string) Role {
|
||||
return Role{
|
||||
Name: "org-member-" + orgID,
|
||||
Org: map[string][]Permission{
|
||||
orgID: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RoleWorkspaceAgent returns a role with permission to read a given
|
||||
// workspace.
|
||||
func RoleWorkspaceAgent(workspaceID string) Role {
|
||||
return Role{
|
||||
Name: fmt.Sprintf("agent-%s", workspaceID),
|
||||
// This is at the site level to prevent the token from losing access if the user
|
||||
// is kicked from the org
|
||||
Site: []Permission{
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: ResourceWorkspace.Type,
|
||||
ResourceID: workspaceID,
|
||||
Action: ActionRead,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func permissions(perms map[Object][]Action) []Permission {
|
||||
list := make([]Permission, 0, len(perms))
|
||||
for k, actions := range perms {
|
||||
for _, act := range actions {
|
||||
act := act
|
||||
list = append(list, Permission{
|
||||
Negate: false,
|
||||
ResourceType: k.Type,
|
||||
ResourceID: WildcardSymbol,
|
||||
Action: act,
|
||||
})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
Reference in New Issue
Block a user