mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: create type for unique role names (#13506)
* chore: create type for unique role names Using `string` was confusing when something should be combined with org context, and when not to. Naming this new name, "RoleIdentifier"
This commit is contained in:
+10
-4
@@ -110,13 +110,13 @@ func (s Subject) SafeScopeName() string {
|
||||
if s.Scope == nil {
|
||||
return "no-scope"
|
||||
}
|
||||
return s.Scope.Name()
|
||||
return s.Scope.Name().String()
|
||||
}
|
||||
|
||||
// SafeRoleNames prevent nil pointer dereference.
|
||||
func (s Subject) SafeRoleNames() []string {
|
||||
func (s Subject) SafeRoleNames() []RoleIdentifier {
|
||||
if s.Roles == nil {
|
||||
return []string{}
|
||||
return []RoleIdentifier{}
|
||||
}
|
||||
return s.Roles.Names()
|
||||
}
|
||||
@@ -707,9 +707,15 @@ func (c *authCache) Prepare(ctx context.Context, subject Subject, action policy.
|
||||
// rbacTraceAttributes are the attributes that are added to all spans created by
|
||||
// the rbac package. These attributes should help to debug slow spans.
|
||||
func rbacTraceAttributes(actor Subject, action policy.Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption {
|
||||
uniqueRoleNames := actor.SafeRoleNames()
|
||||
roleStrings := make([]string, 0, len(uniqueRoleNames))
|
||||
for _, roleName := range uniqueRoleNames {
|
||||
roleName := roleName
|
||||
roleStrings = append(roleStrings, roleName.String())
|
||||
}
|
||||
return trace.WithAttributes(
|
||||
append(extra,
|
||||
attribute.StringSlice("subject_roles", actor.SafeRoleNames()),
|
||||
attribute.StringSlice("subject_roles", roleStrings),
|
||||
attribute.Int("num_subject_roles", len(actor.SafeRoleNames())),
|
||||
attribute.Int("num_groups", len(actor.Groups)),
|
||||
attribute.String("scope", actor.SafeScopeName()),
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestFilterError(t *testing.T) {
|
||||
auth := NewAuthorizer(prometheus.NewRegistry())
|
||||
subject := Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: RoleNames{},
|
||||
Roles: RoleIdentifiers{},
|
||||
Groups: []string{},
|
||||
Scope: ScopeAll,
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func TestFilterError(t *testing.T) {
|
||||
|
||||
subject := Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: RoleNames{
|
||||
Roles: RoleIdentifiers{
|
||||
RoleOwner(),
|
||||
},
|
||||
Groups: []string{},
|
||||
@@ -159,7 +159,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "NoRoles",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{},
|
||||
Roles: RoleIdentifiers{},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -168,7 +168,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "Admin",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{ScopedRoleOrgMember(orgIDs[0]), "auditor", RoleOwner(), RoleMember()},
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), RoleAuditor(), RoleOwner(), RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -177,7 +177,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "OrgAdmin",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgAdmin(orgIDs[0]), RoleMember()},
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgAdmin(orgIDs[0]), RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -186,7 +186,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "OrgMember",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgMember(orgIDs[1]), RoleMember()},
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgMember(orgIDs[1]), RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -195,7 +195,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "ManyRoles",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{
|
||||
Roles: RoleIdentifiers{
|
||||
ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgAdmin(orgIDs[0]),
|
||||
ScopedRoleOrgMember(orgIDs[1]), ScopedRoleOrgAdmin(orgIDs[1]),
|
||||
ScopedRoleOrgMember(orgIDs[2]), ScopedRoleOrgAdmin(orgIDs[2]),
|
||||
@@ -211,7 +211,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "SiteMember",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{RoleMember()},
|
||||
Roles: RoleIdentifiers{RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceUser.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -220,7 +220,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "ReadOrgs",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{
|
||||
Roles: RoleIdentifiers{
|
||||
ScopedRoleOrgMember(orgIDs[0]),
|
||||
ScopedRoleOrgMember(orgIDs[1]),
|
||||
ScopedRoleOrgMember(orgIDs[2]),
|
||||
@@ -235,7 +235,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "ScopeApplicationConnect",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleNames{ScopedRoleOrgMember(orgIDs[0]), "auditor", RoleOwner(), RoleMember()},
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), RoleAuditor(), RoleOwner(), RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -394,7 +394,7 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
ID: "me",
|
||||
Scope: must(ExpandScope(ScopeAll)),
|
||||
Roles: Roles{{
|
||||
Name: "deny-all",
|
||||
Identifier: RoleIdentifier{Name: "deny-all"},
|
||||
// List out deny permissions explicitly
|
||||
Site: []Permission{
|
||||
{
|
||||
@@ -607,8 +607,8 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
Scope: must(ExpandScope(ScopeAll)),
|
||||
Roles: Roles{
|
||||
{
|
||||
Name: "ReadOnlyOrgAndUser",
|
||||
Site: []Permission{},
|
||||
Identifier: RoleIdentifier{Name: "ReadOnlyOrgAndUser"},
|
||||
Site: []Permission{},
|
||||
Org: map[string][]Permission{
|
||||
defOrg.String(): {{
|
||||
Negate: false,
|
||||
@@ -701,7 +701,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleOwner())),
|
||||
{
|
||||
Name: "org-deny:" + defOrg.String(),
|
||||
Identifier: RoleIdentifier{Name: "org-deny:", OrganizationID: defOrg},
|
||||
Org: map[string][]Permission{
|
||||
defOrg.String(): {
|
||||
{
|
||||
@@ -713,7 +713,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-deny-all",
|
||||
Identifier: RoleIdentifier{Name: "user-deny-all"},
|
||||
// List out deny permissions explicitly
|
||||
User: []Permission{
|
||||
{
|
||||
@@ -761,7 +761,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
Scope: must(ExpandScope(ScopeAll)),
|
||||
Roles: Roles{
|
||||
{
|
||||
Name: "site-noise",
|
||||
Identifier: RoleIdentifier{Name: "site-noise"},
|
||||
Site: []Permission{
|
||||
{
|
||||
Negate: true,
|
||||
@@ -772,7 +772,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
},
|
||||
must(RoleByName(ScopedRoleOrgAdmin(defOrg))),
|
||||
{
|
||||
Name: "user-deny-all",
|
||||
Identifier: RoleIdentifier{Name: "user-deny-all"},
|
||||
// List out deny permissions explicitly
|
||||
User: []Permission{
|
||||
{
|
||||
@@ -896,7 +896,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
},
|
||||
Scope: Scope{
|
||||
Role: Role{
|
||||
Name: "workspace_agent",
|
||||
Identifier: RoleIdentifier{Name: "workspace_agent"},
|
||||
DisplayName: "Workspace Agent",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
// Only read access for workspaces.
|
||||
@@ -985,7 +985,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
},
|
||||
Scope: Scope{
|
||||
Role: Role{
|
||||
Name: "create_workspace",
|
||||
Identifier: RoleIdentifier{Name: "create_workspace"},
|
||||
DisplayName: "Create Workspace",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
// Only read access for workspaces.
|
||||
|
||||
+11
-11
@@ -41,7 +41,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "NoRoles",
|
||||
Actor: rbac.Subject{
|
||||
ID: user.String(),
|
||||
Roles: rbac.RoleNames{},
|
||||
Roles: rbac.RoleIdentifiers{},
|
||||
Scope: rbac.ScopeAll,
|
||||
},
|
||||
},
|
||||
@@ -49,7 +49,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "Admin",
|
||||
Actor: rbac.Subject{
|
||||
// Give some extra roles that an admin might have
|
||||
Roles: rbac.RoleNames{rbac.ScopedRoleOrgMember(orgs[0]), "auditor", rbac.RoleOwner(), rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.ScopedRoleOrgMember(orgs[0]), rbac.RoleAuditor(), rbac.RoleOwner(), rbac.RoleMember()},
|
||||
ID: user.String(),
|
||||
Scope: rbac.ScopeAll,
|
||||
Groups: noiseGroups,
|
||||
@@ -58,7 +58,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
{
|
||||
Name: "OrgAdmin",
|
||||
Actor: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.ScopedRoleOrgMember(orgs[0]), rbac.ScopedRoleOrgAdmin(orgs[0]), rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.ScopedRoleOrgMember(orgs[0]), rbac.ScopedRoleOrgAdmin(orgs[0]), rbac.RoleMember()},
|
||||
ID: user.String(),
|
||||
Scope: rbac.ScopeAll,
|
||||
Groups: noiseGroups,
|
||||
@@ -68,7 +68,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "OrgMember",
|
||||
Actor: rbac.Subject{
|
||||
// Member of 2 orgs
|
||||
Roles: rbac.RoleNames{rbac.ScopedRoleOrgMember(orgs[0]), rbac.ScopedRoleOrgMember(orgs[1]), rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.ScopedRoleOrgMember(orgs[0]), rbac.ScopedRoleOrgMember(orgs[1]), rbac.RoleMember()},
|
||||
ID: user.String(),
|
||||
Scope: rbac.ScopeAll,
|
||||
Groups: noiseGroups,
|
||||
@@ -78,7 +78,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "ManyRoles",
|
||||
Actor: rbac.Subject{
|
||||
// Admin of many orgs
|
||||
Roles: rbac.RoleNames{
|
||||
Roles: rbac.RoleIdentifiers{
|
||||
rbac.ScopedRoleOrgMember(orgs[0]), rbac.ScopedRoleOrgAdmin(orgs[0]),
|
||||
rbac.ScopedRoleOrgMember(orgs[1]), rbac.ScopedRoleOrgAdmin(orgs[1]),
|
||||
rbac.ScopedRoleOrgMember(orgs[2]), rbac.ScopedRoleOrgAdmin(orgs[2]),
|
||||
@@ -93,7 +93,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "ManyRolesCachedSubject",
|
||||
Actor: rbac.Subject{
|
||||
// Admin of many orgs
|
||||
Roles: rbac.RoleNames{
|
||||
Roles: rbac.RoleIdentifiers{
|
||||
rbac.ScopedRoleOrgMember(orgs[0]), rbac.ScopedRoleOrgAdmin(orgs[0]),
|
||||
rbac.ScopedRoleOrgMember(orgs[1]), rbac.ScopedRoleOrgAdmin(orgs[1]),
|
||||
rbac.ScopedRoleOrgMember(orgs[2]), rbac.ScopedRoleOrgAdmin(orgs[2]),
|
||||
@@ -108,7 +108,7 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "AdminWithScope",
|
||||
Actor: rbac.Subject{
|
||||
// Give some extra roles that an admin might have
|
||||
Roles: rbac.RoleNames{rbac.ScopedRoleOrgMember(orgs[0]), "auditor", rbac.RoleOwner(), rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.ScopedRoleOrgMember(orgs[0]), rbac.RoleAuditor(), rbac.RoleOwner(), rbac.RoleMember()},
|
||||
ID: user.String(),
|
||||
Scope: rbac.ScopeApplicationConnect,
|
||||
Groups: noiseGroups,
|
||||
@@ -119,8 +119,8 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "StaticRoles",
|
||||
Actor: rbac.Subject{
|
||||
// Give some extra roles that an admin might have
|
||||
Roles: rbac.RoleNames{
|
||||
"auditor", rbac.RoleOwner(), rbac.RoleMember(),
|
||||
Roles: rbac.RoleIdentifiers{
|
||||
rbac.RoleAuditor(), rbac.RoleOwner(), rbac.RoleMember(),
|
||||
rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(),
|
||||
},
|
||||
ID: user.String(),
|
||||
@@ -133,8 +133,8 @@ func benchmarkUserCases() (cases []benchmarkCase, users uuid.UUID, orgs []uuid.U
|
||||
Name: "StaticRolesWithCache",
|
||||
Actor: rbac.Subject{
|
||||
// Give some extra roles that an admin might have
|
||||
Roles: rbac.RoleNames{
|
||||
"auditor", rbac.RoleOwner(), rbac.RoleMember(),
|
||||
Roles: rbac.RoleIdentifiers{
|
||||
rbac.RoleAuditor(), rbac.RoleOwner(), rbac.RoleMember(),
|
||||
rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(),
|
||||
},
|
||||
ID: user.String(),
|
||||
|
||||
+127
-129
@@ -1,6 +1,7 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -34,41 +35,98 @@ func init() {
|
||||
ReloadBuiltinRoles(nil)
|
||||
}
|
||||
|
||||
// RoleNames is a list of user assignable role names. The role names must be
|
||||
// RoleIdentifiers is a list of user assignable role names. The role names must be
|
||||
// in the builtInRoles map. Any non-user assignable roles will generate an
|
||||
// error on Expand.
|
||||
type RoleNames []string
|
||||
type RoleIdentifiers []RoleIdentifier
|
||||
|
||||
func (names RoleNames) Expand() ([]Role, error) {
|
||||
func (names RoleIdentifiers) Expand() ([]Role, error) {
|
||||
return rolesByNames(names)
|
||||
}
|
||||
|
||||
func (names RoleNames) Names() []string {
|
||||
func (names RoleIdentifiers) Names() []RoleIdentifier {
|
||||
return names
|
||||
}
|
||||
|
||||
// RoleIdentifier contains both the name of the role, and any organizational scope.
|
||||
// Both fields are required to be globally unique and identifiable.
|
||||
type RoleIdentifier struct {
|
||||
Name string
|
||||
// OrganizationID is uuid.Nil for unscoped roles (aka deployment wide)
|
||||
OrganizationID uuid.UUID
|
||||
}
|
||||
|
||||
func (r RoleIdentifier) IsOrgRole() bool {
|
||||
return r.OrganizationID != uuid.Nil
|
||||
}
|
||||
|
||||
// RoleNameFromString takes a formatted string '<role_name>[:org_id]'.
|
||||
func RoleNameFromString(input string) (RoleIdentifier, error) {
|
||||
var role RoleIdentifier
|
||||
|
||||
arr := strings.Split(input, ":")
|
||||
if len(arr) > 2 {
|
||||
return role, xerrors.Errorf("too many colons in role name")
|
||||
}
|
||||
|
||||
if len(arr) == 0 {
|
||||
return role, xerrors.Errorf("empty string not a valid role")
|
||||
}
|
||||
|
||||
if arr[0] == "" {
|
||||
return role, xerrors.Errorf("role cannot be the empty string")
|
||||
}
|
||||
|
||||
role.Name = arr[0]
|
||||
|
||||
if len(arr) == 2 {
|
||||
orgID, err := uuid.Parse(arr[1])
|
||||
if err != nil {
|
||||
return role, xerrors.Errorf("%q not a valid uuid: %w", arr[1], err)
|
||||
}
|
||||
role.OrganizationID = orgID
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (r RoleIdentifier) String() string {
|
||||
if r.OrganizationID != uuid.Nil {
|
||||
return r.Name + ":" + r.OrganizationID.String()
|
||||
}
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *RoleIdentifier) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(r.String())
|
||||
}
|
||||
|
||||
func (r *RoleIdentifier) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
err := json.Unmarshal(data, &str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := RoleNameFromString(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*r = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 RoleOwner() string {
|
||||
return RoleName(owner, "")
|
||||
}
|
||||
|
||||
func CustomSiteRole() string { return RoleName(customSiteRole, "") }
|
||||
|
||||
func RoleTemplateAdmin() string {
|
||||
return RoleName(templateAdmin, "")
|
||||
}
|
||||
|
||||
func RoleUserAdmin() string {
|
||||
return RoleName(userAdmin, "")
|
||||
}
|
||||
|
||||
func RoleMember() string {
|
||||
return RoleName(member, "")
|
||||
}
|
||||
func RoleOwner() RoleIdentifier { return RoleIdentifier{Name: owner} }
|
||||
func CustomSiteRole() RoleIdentifier { return RoleIdentifier{Name: customSiteRole} }
|
||||
func RoleTemplateAdmin() RoleIdentifier { return RoleIdentifier{Name: templateAdmin} }
|
||||
func RoleUserAdmin() RoleIdentifier { return RoleIdentifier{Name: userAdmin} }
|
||||
func RoleMember() RoleIdentifier { return RoleIdentifier{Name: member} }
|
||||
func RoleAuditor() RoleIdentifier { return RoleIdentifier{Name: auditor} }
|
||||
|
||||
func RoleOrgAdmin() string {
|
||||
return orgAdmin
|
||||
@@ -81,15 +139,15 @@ func RoleOrgMember() string {
|
||||
// ScopedRoleOrgAdmin is the org role with the organization ID
|
||||
// Deprecated This was used before organization scope was included as a
|
||||
// field in all user facing APIs. Usage of 'ScopedRoleOrgAdmin()' is preferred.
|
||||
func ScopedRoleOrgAdmin(organizationID uuid.UUID) string {
|
||||
return RoleName(orgAdmin, organizationID.String())
|
||||
func ScopedRoleOrgAdmin(organizationID uuid.UUID) RoleIdentifier {
|
||||
return RoleIdentifier{Name: orgAdmin, OrganizationID: organizationID}
|
||||
}
|
||||
|
||||
// ScopedRoleOrgMember is the org role with the organization ID
|
||||
// Deprecated This was used before organization scope was included as a
|
||||
// field in all user facing APIs. Usage of 'ScopedRoleOrgMember()' is preferred.
|
||||
func ScopedRoleOrgMember(organizationID uuid.UUID) string {
|
||||
return RoleName(orgMember, organizationID.String())
|
||||
func ScopedRoleOrgMember(organizationID uuid.UUID) RoleIdentifier {
|
||||
return RoleIdentifier{Name: orgMember, OrganizationID: organizationID}
|
||||
}
|
||||
|
||||
func allPermsExcept(excepts ...Objecter) []Permission {
|
||||
@@ -127,7 +185,7 @@ func allPermsExcept(excepts ...Objecter) []Permission {
|
||||
//
|
||||
// This map will be replaced by database storage defined by this ticket.
|
||||
// https://github.com/coder/coder/issues/1194
|
||||
var builtInRoles map[string]func(orgID string) Role
|
||||
var builtInRoles map[string]func(orgID uuid.UUID) Role
|
||||
|
||||
type RoleOptions struct {
|
||||
NoOwnerWorkspaceExec bool
|
||||
@@ -158,7 +216,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
// on every authorize call. 'withCachedRegoValue' can be used as well to
|
||||
// preallocate the rego value that is used by the rego eval engine.
|
||||
ownerRole := Role{
|
||||
Name: owner,
|
||||
Identifier: RoleOwner(),
|
||||
DisplayName: "Owner",
|
||||
Site: append(
|
||||
// Workspace dormancy and workspace are omitted.
|
||||
@@ -174,7 +232,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
}.withCachedRegoValue()
|
||||
|
||||
memberRole := Role{
|
||||
Name: member,
|
||||
Identifier: RoleMember(),
|
||||
DisplayName: "Member",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceAssignRole.Type: {policy.ActionRead},
|
||||
@@ -200,7 +258,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
}.withCachedRegoValue()
|
||||
|
||||
auditorRole := Role{
|
||||
Name: auditor,
|
||||
Identifier: RoleAuditor(),
|
||||
DisplayName: "Auditor",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
// Should be able to read all template details, even in orgs they
|
||||
@@ -220,7 +278,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
}.withCachedRegoValue()
|
||||
|
||||
templateAdminRole := Role{
|
||||
Name: templateAdmin,
|
||||
Identifier: RoleTemplateAdmin(),
|
||||
DisplayName: "Template Admin",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceTemplate.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights},
|
||||
@@ -241,7 +299,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
}.withCachedRegoValue()
|
||||
|
||||
userAdminRole := Role{
|
||||
Name: userAdmin,
|
||||
Identifier: RoleUserAdmin(),
|
||||
DisplayName: "User Admin",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead},
|
||||
@@ -257,42 +315,42 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
User: []Permission{},
|
||||
}.withCachedRegoValue()
|
||||
|
||||
builtInRoles = map[string]func(orgID string) Role{
|
||||
builtInRoles = map[string]func(orgID uuid.UUID) Role{
|
||||
// admin grants all actions to all resources.
|
||||
owner: func(_ string) Role {
|
||||
owner: func(_ uuid.UUID) Role {
|
||||
return ownerRole
|
||||
},
|
||||
|
||||
// member grants all actions to all resources owned by the user
|
||||
member: func(_ string) Role {
|
||||
member: func(_ uuid.UUID) Role {
|
||||
return memberRole
|
||||
},
|
||||
|
||||
// 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 {
|
||||
auditor: func(_ uuid.UUID) Role {
|
||||
return auditorRole
|
||||
},
|
||||
|
||||
templateAdmin: func(_ string) Role {
|
||||
templateAdmin: func(_ uuid.UUID) Role {
|
||||
return templateAdminRole
|
||||
},
|
||||
|
||||
userAdmin: func(_ string) Role {
|
||||
userAdmin: func(_ uuid.UUID) Role {
|
||||
return userAdminRole
|
||||
},
|
||||
|
||||
// orgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
orgAdmin: func(organizationID string) Role {
|
||||
orgAdmin: func(organizationID uuid.UUID) Role {
|
||||
return Role{
|
||||
Name: RoleName(orgAdmin, organizationID),
|
||||
Identifier: RoleIdentifier{Name: orgAdmin, OrganizationID: organizationID},
|
||||
DisplayName: "Organization Admin",
|
||||
Site: []Permission{},
|
||||
Org: map[string][]Permission{
|
||||
// Org admins should not have workspace exec perms.
|
||||
organizationID: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant), Permissions(map[string][]policy.Action{
|
||||
organizationID.String(): 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),
|
||||
})...),
|
||||
@@ -303,13 +361,13 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
|
||||
// orgMember has an empty set of permissions, this just implies their membership
|
||||
// in an organization.
|
||||
orgMember: func(organizationID string) Role {
|
||||
orgMember: func(organizationID uuid.UUID) Role {
|
||||
return Role{
|
||||
Name: RoleName(orgMember, organizationID),
|
||||
Identifier: RoleIdentifier{Name: orgMember, OrganizationID: organizationID},
|
||||
DisplayName: "",
|
||||
Site: []Permission{},
|
||||
Org: map[string][]Permission{
|
||||
organizationID: {
|
||||
organizationID.String(): {
|
||||
{
|
||||
// All org members can read the organization
|
||||
ResourceType: ResourceOrganization.Type,
|
||||
@@ -370,7 +428,7 @@ var assignRoles = map[string]map[string]bool{
|
||||
}
|
||||
|
||||
// ExpandableRoles is any type that can be expanded into a []Role. This is implemented
|
||||
// as an interface so we can have RoleNames for user defined roles, and implement
|
||||
// as an interface so we can have RoleIdentifiers for user defined roles, and implement
|
||||
// custom ExpandableRoles for system type users (eg autostart/autostop system role).
|
||||
// We want a clear divide between the two types of roles so users have no codepath
|
||||
// to interact or assign system roles.
|
||||
@@ -381,7 +439,7 @@ type ExpandableRoles interface {
|
||||
Expand() ([]Role, error)
|
||||
// Names is for logging and tracing purposes, we want to know the human
|
||||
// names of the expanded roles.
|
||||
Names() []string
|
||||
Names() []RoleIdentifier
|
||||
}
|
||||
|
||||
// Permission is the format passed into the rego.
|
||||
@@ -424,7 +482,7 @@ func (perm Permission) Valid() error {
|
||||
// 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"`
|
||||
Identifier RoleIdentifier `json:"name"`
|
||||
// DisplayName is used for UI purposes. If the role has no display name,
|
||||
// that means the UI should never display it.
|
||||
DisplayName string `json:"display_name"`
|
||||
@@ -474,10 +532,10 @@ func (roles Roles) Expand() ([]Role, error) {
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (roles Roles) Names() []string {
|
||||
names := make([]string, 0, len(roles))
|
||||
func (roles Roles) Names() []RoleIdentifier {
|
||||
names := make([]RoleIdentifier, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
names = append(names, r.Name)
|
||||
names = append(names, r.Identifier)
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -485,32 +543,22 @@ func (roles Roles) Names() []string {
|
||||
// CanAssignRole is a helper function that returns true if the user can assign
|
||||
// the specified role. This also can be used for removing a role.
|
||||
// This is a simple implementation for now.
|
||||
func CanAssignRole(expandable ExpandableRoles, assignedRole string) bool {
|
||||
func CanAssignRole(subjectHasRoles ExpandableRoles, assignedRole RoleIdentifier) bool {
|
||||
// For CanAssignRole, we only care about the names of the roles.
|
||||
roles := expandable.Names()
|
||||
roles := subjectHasRoles.Names()
|
||||
|
||||
assigned, assignedOrg, err := RoleSplit(assignedRole)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, longRole := range roles {
|
||||
role, orgID, err := RoleSplit(longRole)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if orgID != "" && orgID != assignedOrg {
|
||||
for _, myRole := range roles {
|
||||
if myRole.OrganizationID != uuid.Nil && myRole.OrganizationID != assignedRole.OrganizationID {
|
||||
// Org roles only apply to the org they are assigned to.
|
||||
continue
|
||||
}
|
||||
|
||||
allowed, ok := assignRoles[role]
|
||||
allowedAssignList, ok := assignRoles[myRole.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if allowed[assigned] {
|
||||
if allowedAssignList[assignedRole.Name] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -523,29 +571,24 @@ func CanAssignRole(expandable ExpandableRoles, assignedRole string) bool {
|
||||
// This function is exported so that the Display name can be returned to the
|
||||
// api. We should maybe make an exported function that returns just the
|
||||
// human-readable content of the Role struct (name + display name).
|
||||
func RoleByName(name string) (Role, error) {
|
||||
roleName, orgID, err := RoleSplit(name)
|
||||
if err != nil {
|
||||
return Role{}, xerrors.Errorf("parse role name: %w", err)
|
||||
}
|
||||
|
||||
roleFunc, ok := builtInRoles[roleName]
|
||||
func RoleByName(name RoleIdentifier) (Role, error) {
|
||||
roleFunc, ok := builtInRoles[name.Name]
|
||||
if !ok {
|
||||
// No role found
|
||||
return Role{}, xerrors.Errorf("role %q not found", roleName)
|
||||
return Role{}, xerrors.Errorf("role %q not found", name.String())
|
||||
}
|
||||
|
||||
// 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)
|
||||
role := roleFunc(name.OrganizationID)
|
||||
if len(role.Org) > 0 && name.OrganizationID == uuid.Nil {
|
||||
return Role{}, xerrors.Errorf("expect a org id for role %q", name.String())
|
||||
}
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func rolesByNames(roleNames []string) ([]Role, error) {
|
||||
func rolesByNames(roleNames []RoleIdentifier) ([]Role, error) {
|
||||
roles := make([]Role, 0, len(roleNames))
|
||||
for _, n := range roleNames {
|
||||
r, err := RoleByName(n)
|
||||
@@ -557,14 +600,6 @@ func rolesByNames(roleNames []string) ([]Role, error) {
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func IsOrgRole(roleName string) (string, bool) {
|
||||
_, orgID, err := RoleSplit(roleName)
|
||||
if err == nil && orgID != "" {
|
||||
return orgID, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// OrganizationRoles lists all roles that can be applied to an organization user
|
||||
// in the given organization. This is the list of available roles,
|
||||
// and specific to an organization.
|
||||
@@ -574,13 +609,8 @@ func IsOrgRole(roleName string) (string, bool) {
|
||||
func OrganizationRoles(organizationID uuid.UUID) []Role {
|
||||
var roles []Role
|
||||
for _, roleF := range builtInRoles {
|
||||
role := roleF(organizationID.String())
|
||||
_, scope, err := RoleSplit(role.Name)
|
||||
if err != nil {
|
||||
// This should never happen
|
||||
continue
|
||||
}
|
||||
if scope == organizationID.String() {
|
||||
role := roleF(organizationID)
|
||||
if role.Identifier.OrganizationID == organizationID {
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
@@ -595,13 +625,9 @@ func OrganizationRoles(organizationID uuid.UUID) []Role {
|
||||
func SiteRoles() []Role {
|
||||
var roles []Role
|
||||
for _, roleF := range builtInRoles {
|
||||
role := roleF("random")
|
||||
_, scope, err := RoleSplit(role.Name)
|
||||
if err != nil {
|
||||
// This should never happen
|
||||
continue
|
||||
}
|
||||
if scope == "" {
|
||||
// Must provide some non-nil uuid to filter out org roles.
|
||||
role := roleF(uuid.New())
|
||||
if !role.Identifier.IsOrgRole() {
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
@@ -613,8 +639,8 @@ func SiteRoles() []Role {
|
||||
// removing roles. This set determines the changes, so that the appropriate
|
||||
// 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{})
|
||||
func ChangeRoleSet(from []RoleIdentifier, to []RoleIdentifier) (added []RoleIdentifier, removed []RoleIdentifier) {
|
||||
has := make(map[RoleIdentifier]struct{})
|
||||
for _, exists := range from {
|
||||
has[exists] = struct{}{}
|
||||
}
|
||||
@@ -639,34 +665,6 @@ func ChangeRoleSet(from []string, to []string) (added []string, removed []string
|
||||
return added, removed
|
||||
}
|
||||
|
||||
// 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[string][]policy.Action) []Permission {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// A possible large improvement would be to implement the ast.Value interface directly.
|
||||
func BenchmarkRBACValueAllocation(b *testing.B) {
|
||||
actor := Subject{
|
||||
Roles: RoleNames{ScopedRoleOrgMember(uuid.New()), ScopedRoleOrgAdmin(uuid.New()), RoleMember()},
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(uuid.New()), ScopedRoleOrgAdmin(uuid.New()), RoleMember()},
|
||||
ID: uuid.NewString(),
|
||||
Scope: ScopeAll,
|
||||
Groups: []string{uuid.NewString(), uuid.NewString(), uuid.NewString()},
|
||||
@@ -73,7 +73,7 @@ func TestRegoInputValue(t *testing.T) {
|
||||
// Expand all roles and make sure we have a good copy.
|
||||
// This is because these tests modify the roles, and we don't want to
|
||||
// modify the original roles.
|
||||
roles, err := RoleNames{ScopedRoleOrgMember(uuid.New()), ScopedRoleOrgAdmin(uuid.New()), RoleMember()}.Expand()
|
||||
roles, err := RoleIdentifiers{ScopedRoleOrgMember(uuid.New()), ScopedRoleOrgAdmin(uuid.New()), RoleMember()}.Expand()
|
||||
require.NoError(t, err, "failed to expand roles")
|
||||
for i := range roles {
|
||||
// If all cached values are nil, then the role will not use
|
||||
@@ -213,25 +213,25 @@ func TestRoleByName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Role Role
|
||||
}{
|
||||
{Role: builtInRoles[owner]("")},
|
||||
{Role: builtInRoles[member]("")},
|
||||
{Role: builtInRoles[templateAdmin]("")},
|
||||
{Role: builtInRoles[userAdmin]("")},
|
||||
{Role: builtInRoles[auditor]("")},
|
||||
{Role: builtInRoles[owner](uuid.Nil)},
|
||||
{Role: builtInRoles[member](uuid.Nil)},
|
||||
{Role: builtInRoles[templateAdmin](uuid.Nil)},
|
||||
{Role: builtInRoles[userAdmin](uuid.Nil)},
|
||||
{Role: builtInRoles[auditor](uuid.Nil)},
|
||||
|
||||
{Role: builtInRoles[orgAdmin]("4592dac5-0945-42fd-828d-a903957d3dbb")},
|
||||
{Role: builtInRoles[orgAdmin]("24c100c5-1920-49c0-8c38-1b640ac4b38c")},
|
||||
{Role: builtInRoles[orgAdmin]("4a00f697-0040-4079-b3ce-d24470281a62")},
|
||||
{Role: builtInRoles[orgAdmin](uuid.New())},
|
||||
{Role: builtInRoles[orgAdmin](uuid.New())},
|
||||
{Role: builtInRoles[orgAdmin](uuid.New())},
|
||||
|
||||
{Role: builtInRoles[orgMember]("3293c50e-fa5d-414f-a461-01112a4dfb6f")},
|
||||
{Role: builtInRoles[orgMember]("f88dd23d-bdbd-469d-b82e-36ee06c3d1e1")},
|
||||
{Role: builtInRoles[orgMember]("02cfd2a5-016c-4d8d-8290-301f5f18023d")},
|
||||
{Role: builtInRoles[orgMember](uuid.New())},
|
||||
{Role: builtInRoles[orgMember](uuid.New())},
|
||||
{Role: builtInRoles[orgMember](uuid.New())},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.Role.Name, func(t *testing.T) {
|
||||
role, err := RoleByName(c.Role.Name)
|
||||
t.Run(c.Role.Identifier.String(), func(t *testing.T) {
|
||||
role, err := RoleByName(c.Role.Identifier)
|
||||
require.NoError(t, err, "role exists")
|
||||
equalRoles(t, c.Role, role)
|
||||
})
|
||||
@@ -242,20 +242,17 @@ func TestRoleByName(t *testing.T) {
|
||||
t.Run("Errors", func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
_, err = RoleByName("")
|
||||
_, err = RoleByName(RoleIdentifier{})
|
||||
require.Error(t, err, "empty role")
|
||||
|
||||
_, err = RoleByName("too:many:colons")
|
||||
require.Error(t, err, "too many colons")
|
||||
|
||||
_, err = RoleByName(orgMember)
|
||||
_, err = RoleByName(RoleIdentifier{Name: orgMember})
|
||||
require.Error(t, err, "expect orgID")
|
||||
})
|
||||
}
|
||||
|
||||
// SameAs compares 2 roles for equality.
|
||||
func equalRoles(t *testing.T, a, b Role) {
|
||||
require.Equal(t, a.Name, b.Name, "role names")
|
||||
require.Equal(t, a.Identifier, b.Identifier, "role names")
|
||||
require.Equal(t, a.DisplayName, b.DisplayName, "role display names")
|
||||
require.ElementsMatch(t, a.Site, b.Site, "site permissions")
|
||||
require.ElementsMatch(t, a.User, b.User, "user permissions")
|
||||
|
||||
+42
-43
@@ -26,7 +26,7 @@ func TestBuiltInRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, r := range rbac.SiteRoles() {
|
||||
r := r
|
||||
t.Run(r.Name, func(t *testing.T) {
|
||||
t.Run(r.Identifier.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NoError(t, r.Valid(), "invalid role")
|
||||
})
|
||||
@@ -34,7 +34,7 @@ func TestBuiltInRoles(t *testing.T) {
|
||||
|
||||
for _, r := range rbac.OrganizationRoles(uuid.New()) {
|
||||
r := r
|
||||
t.Run(r.Name, func(t *testing.T) {
|
||||
t.Run(r.Identifier.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NoError(t, r.Valid(), "invalid role")
|
||||
})
|
||||
@@ -45,7 +45,7 @@ func TestBuiltInRoles(t *testing.T) {
|
||||
func TestOwnerExec(t *testing.T) {
|
||||
owner := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleOwner()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()},
|
||||
Scope: rbac.ScopeAll,
|
||||
}
|
||||
|
||||
@@ -98,17 +98,17 @@ func TestRolePermissions(t *testing.T) {
|
||||
apiKeyID := uuid.New()
|
||||
|
||||
// Subjects to user
|
||||
memberMe := authSubject{Name: "member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleNames{rbac.RoleMember()}}}
|
||||
orgMemberMe := authSubject{Name: "org_member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleNames{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID)}}}
|
||||
memberMe := authSubject{Name: "member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember()}}}
|
||||
orgMemberMe := authSubject{Name: "org_member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID)}}}
|
||||
|
||||
owner := authSubject{Name: "owner", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleOwner()}}}
|
||||
orgAdmin := authSubject{Name: "org_admin", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleNames{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAdmin(orgID)}}}
|
||||
owner := authSubject{Name: "owner", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()}}}
|
||||
orgAdmin := authSubject{Name: "org_admin", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAdmin(orgID)}}}
|
||||
|
||||
otherOrgMember := authSubject{Name: "org_member_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleNames{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg)}}}
|
||||
otherOrgAdmin := authSubject{Name: "org_admin_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleNames{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg), rbac.ScopedRoleOrgAdmin(otherOrg)}}}
|
||||
otherOrgMember := authSubject{Name: "org_member_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg)}}}
|
||||
otherOrgAdmin := authSubject{Name: "org_admin_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg), rbac.ScopedRoleOrgAdmin(otherOrg)}}}
|
||||
|
||||
templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}}
|
||||
userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleUserAdmin()}}}
|
||||
templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}}
|
||||
userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleUserAdmin()}}}
|
||||
|
||||
// requiredSubjects are required to be asserted in each test case. This is
|
||||
// to make sure one is not forgotten.
|
||||
@@ -616,50 +616,40 @@ func TestIsOrgRole(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
RoleName string
|
||||
OrgRole bool
|
||||
OrgID string
|
||||
Identifier rbac.RoleIdentifier
|
||||
OrgRole bool
|
||||
OrgID uuid.UUID
|
||||
}{
|
||||
// Not org roles
|
||||
{RoleName: rbac.RoleOwner()},
|
||||
{RoleName: rbac.RoleMember()},
|
||||
{RoleName: "auditor"},
|
||||
|
||||
{Identifier: rbac.RoleOwner()},
|
||||
{Identifier: rbac.RoleMember()},
|
||||
{Identifier: rbac.RoleAuditor()},
|
||||
{
|
||||
RoleName: "a:bad:role",
|
||||
OrgRole: false,
|
||||
},
|
||||
{
|
||||
RoleName: "",
|
||||
OrgRole: false,
|
||||
Identifier: rbac.RoleIdentifier{},
|
||||
OrgRole: false,
|
||||
},
|
||||
|
||||
// Org roles
|
||||
{
|
||||
RoleName: rbac.ScopedRoleOrgAdmin(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID.String(),
|
||||
Identifier: rbac.ScopedRoleOrgAdmin(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID,
|
||||
},
|
||||
{
|
||||
RoleName: rbac.ScopedRoleOrgMember(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID.String(),
|
||||
},
|
||||
{
|
||||
RoleName: "test:example",
|
||||
OrgRole: true,
|
||||
OrgID: "example",
|
||||
Identifier: rbac.ScopedRoleOrgMember(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID,
|
||||
},
|
||||
}
|
||||
|
||||
// nolint:paralleltest
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.RoleName, func(t *testing.T) {
|
||||
t.Run(c.Identifier.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
orgID, ok := rbac.IsOrgRole(c.RoleName)
|
||||
ok := c.Identifier.IsOrgRole()
|
||||
require.Equal(t, c.OrgRole, ok, "match expected org role")
|
||||
require.Equal(t, c.OrgID, orgID, "match expected org id")
|
||||
require.Equal(t, c.OrgID, c.Identifier.OrganizationID, "match expected org id")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -670,7 +660,7 @@ func TestListRoles(t *testing.T) {
|
||||
siteRoles := rbac.SiteRoles()
|
||||
siteRoleNames := make([]string, 0, len(siteRoles))
|
||||
for _, role := range siteRoles {
|
||||
siteRoleNames = append(siteRoleNames, role.Name)
|
||||
siteRoleNames = append(siteRoleNames, role.Identifier.Name)
|
||||
}
|
||||
|
||||
// If this test is ever failing, just update the list to the roles
|
||||
@@ -690,7 +680,7 @@ func TestListRoles(t *testing.T) {
|
||||
orgRoles := rbac.OrganizationRoles(orgID)
|
||||
orgRoleNames := make([]string, 0, len(orgRoles))
|
||||
for _, role := range orgRoles {
|
||||
orgRoleNames = append(orgRoleNames, role.Name)
|
||||
orgRoleNames = append(orgRoleNames, role.Identifier.String())
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, []string{
|
||||
@@ -738,13 +728,22 @@ func TestChangeSet(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
convert := func(s []string) rbac.RoleIdentifiers {
|
||||
tmp := make([]rbac.RoleIdentifier, 0, len(s))
|
||||
for _, e := range s {
|
||||
tmp = append(tmp, rbac.RoleIdentifier{Name: e})
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
add, remove := rbac.ChangeRoleSet(c.From, c.To)
|
||||
require.ElementsMatch(t, c.ExpAdd, add, "expect added")
|
||||
require.ElementsMatch(t, c.ExpRemove, remove, "expect removed")
|
||||
|
||||
add, remove := rbac.ChangeRoleSet(convert(c.From), convert(c.To))
|
||||
require.ElementsMatch(t, convert(c.ExpAdd), add, "expect added")
|
||||
require.ElementsMatch(t, convert(c.ExpRemove), remove, "expect removed")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ func roleCache(ctx context.Context) *syncmap.Map[string, rbac.Role] {
|
||||
}
|
||||
|
||||
// Expand will expand built in roles, and fetch custom roles from the database.
|
||||
func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles, error) {
|
||||
func Expand(ctx context.Context, db database.Store, names []rbac.RoleIdentifier) (rbac.Roles, error) {
|
||||
if len(names) == 0 {
|
||||
// That was easy
|
||||
return []rbac.Role{}, nil
|
||||
}
|
||||
|
||||
cache := roleCache(ctx)
|
||||
lookup := make([]string, 0)
|
||||
lookup := make([]rbac.RoleIdentifier, 0)
|
||||
roles := make([]rbac.Role, 0, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
@@ -58,7 +58,7 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles,
|
||||
}
|
||||
|
||||
// Check custom role cache
|
||||
customRole, ok := cache.Load(name)
|
||||
customRole, ok := cache.Load(name.String())
|
||||
if ok {
|
||||
roles = append(roles, customRole)
|
||||
continue
|
||||
@@ -69,26 +69,11 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles,
|
||||
}
|
||||
|
||||
if len(lookup) > 0 {
|
||||
// The set of roles coming in are formatted as 'rolename[:<org_id>]'.
|
||||
// In the database, org roles are scoped with an organization column.
|
||||
lookupArgs := make([]database.NameOrganizationPair, 0, len(lookup))
|
||||
for _, name := range lookup {
|
||||
roleName, orgID, err := rbac.RoleSplit(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedOrgID := uuid.Nil // Default to no org ID
|
||||
if orgID != "" {
|
||||
parsedOrgID, err = uuid.Parse(orgID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
lookupArgs = append(lookupArgs, database.NameOrganizationPair{
|
||||
Name: roleName,
|
||||
OrganizationID: parsedOrgID,
|
||||
Name: name.Name,
|
||||
OrganizationID: name.OrganizationID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,7 +96,7 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles,
|
||||
return nil, xerrors.Errorf("convert db role %q: %w", dbrole.Name, err)
|
||||
}
|
||||
roles = append(roles, converted)
|
||||
cache.Store(dbrole.Name, converted)
|
||||
cache.Store(dbrole.RoleIdentifier().String(), converted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +118,8 @@ func convertPermissions(dbPerms []database.CustomRolePermission) []rbac.Permissi
|
||||
// ConvertDBRole should not be used by any human facing apis. It is used
|
||||
// for authz purposes.
|
||||
func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
||||
name := dbRole.Name
|
||||
if dbRole.OrganizationID.Valid {
|
||||
name = rbac.RoleName(dbRole.Name, dbRole.OrganizationID.UUID.String())
|
||||
}
|
||||
role := rbac.Role{
|
||||
Name: name,
|
||||
Identifier: dbRole.RoleIdentifier(),
|
||||
DisplayName: dbRole.DisplayName,
|
||||
Site: convertPermissions(dbRole.SitePermissions),
|
||||
Org: nil,
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestExpandCustomRoleRoles(t *testing.T) {
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
roles, err := rolestore.Expand(ctx, db, []string{rbac.RoleName(roleName, org.ID.String())})
|
||||
roles, err := rolestore.Expand(ctx, db, []rbac.RoleIdentifier{{Name: roleName, OrganizationID: org.ID}})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, roles, 1, "role found")
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ var builtinScopes = map[ScopeName]Scope{
|
||||
// authorize checks it is usually not used directly and skips scope checks.
|
||||
ScopeAll: {
|
||||
Role: Role{
|
||||
Name: fmt.Sprintf("Scope_%s", ScopeAll),
|
||||
Identifier: RoleIdentifier{Name: fmt.Sprintf("Scope_%s", ScopeAll)},
|
||||
DisplayName: "All operations",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceWildcard.Type: {policy.WildcardSymbol},
|
||||
@@ -71,7 +71,7 @@ var builtinScopes = map[ScopeName]Scope{
|
||||
|
||||
ScopeApplicationConnect: {
|
||||
Role: Role{
|
||||
Name: fmt.Sprintf("Scope_%s", ScopeApplicationConnect),
|
||||
Identifier: RoleIdentifier{Name: fmt.Sprintf("Scope_%s", ScopeApplicationConnect)},
|
||||
DisplayName: "Ability to connect to applications",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceWorkspace.Type: {policy.ActionApplicationConnect},
|
||||
@@ -87,7 +87,7 @@ type ExpandableScope interface {
|
||||
Expand() (Scope, error)
|
||||
// Name is for logging and tracing purposes, we want to know the human
|
||||
// name of the scope.
|
||||
Name() string
|
||||
Name() RoleIdentifier
|
||||
}
|
||||
|
||||
type ScopeName string
|
||||
@@ -96,8 +96,8 @@ func (name ScopeName) Expand() (Scope, error) {
|
||||
return ExpandScope(name)
|
||||
}
|
||||
|
||||
func (name ScopeName) Name() string {
|
||||
return string(name)
|
||||
func (name ScopeName) Name() RoleIdentifier {
|
||||
return RoleIdentifier{Name: string(name)}
|
||||
}
|
||||
|
||||
// Scope acts the exact same as a Role with the addition that is can also
|
||||
@@ -114,8 +114,8 @@ func (s Scope) Expand() (Scope, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s Scope) Name() string {
|
||||
return s.Role.Name
|
||||
func (s Scope) Name() RoleIdentifier {
|
||||
return s.Role.Identifier
|
||||
}
|
||||
|
||||
func ExpandScope(scope ScopeName) (Scope, error) {
|
||||
|
||||
@@ -24,13 +24,13 @@ func TestSubjectEqual(t *testing.T) {
|
||||
Name: "Same",
|
||||
A: rbac.Subject{
|
||||
ID: "id",
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleMember()},
|
||||
Groups: []string{"group"},
|
||||
Scope: rbac.ScopeAll,
|
||||
},
|
||||
B: rbac.Subject{
|
||||
ID: "id",
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleMember()},
|
||||
Groups: []string{"group"},
|
||||
Scope: rbac.ScopeAll,
|
||||
},
|
||||
@@ -49,7 +49,7 @@ func TestSubjectEqual(t *testing.T) {
|
||||
{
|
||||
Name: "RolesNilVs0",
|
||||
A: rbac.Subject{
|
||||
Roles: rbac.RoleNames{},
|
||||
Roles: rbac.RoleIdentifiers{},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Roles: nil,
|
||||
@@ -69,20 +69,20 @@ func TestSubjectEqual(t *testing.T) {
|
||||
{
|
||||
Name: "DifferentRoles",
|
||||
A: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleMember()},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "Different#Roles",
|
||||
A: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleMember()},
|
||||
},
|
||||
B: rbac.Subject{
|
||||
Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleOwner()},
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()},
|
||||
},
|
||||
Expected: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user