Skip to content

Commit

Permalink
Improve RBAC
Browse files Browse the repository at this point in the history
Add a clear set of permissions that are granted to each role.  This
allows fine-grain control at a per-aendpoint level.
  • Loading branch information
spjmurray committed Mar 27, 2024
1 parent 65accce commit f6a5ad6
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/merge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
tags-ignore:
- '*'
env:
GO_VERSION: 1.21.1
GO_VERSION: 1.22.1
jobs:
Coverage:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
tags-ignore:
- '*'
env:
GO_VERSION: 1.21.1
GO_VERSION: 1.22.1
jobs:
Static:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions charts/core/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A Helm chart for deploying Unikorn Core

type: application

version: v0.1.15
appVersion: v0.1.15
version: v0.1.16
appVersion: v0.1.16

icon: https://assets.unikorn-cloud.org/images/logos/dark-on-light/icon.svg
73 changes: 41 additions & 32 deletions pkg/authorization/rbac/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package rbac
import (
"errors"
"fmt"
"slices"

"github.com/unikorn-cloud/core/pkg/authorization/roles"
)
Expand All @@ -28,67 +27,77 @@ var (
ErrPermissionDenied = errors.New("access denied")
)

type AdminAuthorizer struct{}
// SuperAdminAuthorizer allows access to everything.
type SuperAdminAuthorizer struct{}

func (a *AdminAuthorizer) AllowedByRole(_ roles.Role) error {
return nil
}

func (a *AdminAuthorizer) AllowedByGroup(_ []string) error {
return nil
}

func (a *AdminAuthorizer) AllowedByGroupRole(_ []string, _ roles.Role) error {
func (a *SuperAdminAuthorizer) Allow(_ string, _ roles.Permission) error {
return nil
}

// OrganizationAuthorizer is scoped to a specific organization.
type OrganizationAuthorizer struct {
permissions *OrganizationPermissions
}

func (a *OrganizationAuthorizer) AllowedByRole(role roles.Role) error {
return ErrPermissionDenied
}
type PermissionMap map[roles.Permission]interface{}

type ScopedPermissionMap map[string]PermissionMap

func (a *OrganizationAuthorizer) Allow(scope string, permission roles.Permission) error {
roleManager := roles.New()

func (a *OrganizationAuthorizer) AllowedByGroup(groups []string) error {
scopedPermissions := ScopedPermissionMap{}

// Build up a set of scopes and permissions based on group membership and the roles
// associated with that.
for _, group := range a.permissions.Groups {
if slices.Contains(groups, group.ID) {
return nil
for _, r := range group.Roles {
rolePermissions, err := roleManager.GetRole(r)
if err != nil {
return err
}

for roleScope, perms := range rolePermissions.Permissions {
if _, ok := scopedPermissions[roleScope]; !ok {
scopedPermissions[scope] = PermissionMap{}
}

for _, perm := range perms {
scopedPermissions[roleScope][perm] = nil
}
}
}
}

return ErrPermissionDenied
}
s, ok := scopedPermissions[scope]
if !ok {
return fmt.Errorf("%w: not permitted to access the %v scope", ErrPermissionDenied, scope)
}

func (a *OrganizationAuthorizer) AllowedByGroupRole(groups []string, role roles.Role) error {
for _, group := range a.permissions.Groups {
if slices.Contains(groups, group.ID) && slices.Contains(group.Roles, role) {
return nil
}
if _, ok := s[permission]; !ok {
return fmt.Errorf("%w: not permitted to %v within the %v scope", ErrPermissionDenied, permission, scope)
}

return ErrPermissionDenied
return nil
}

func NewAuthorizer(permissions *Permissions, organizationName string) (Authorizer, error) {
func New(permissions *Permissions, organizationName string) (Authorizer, error) {
if permissions == nil {
return nil, fmt.Errorf("%w: user has no RBAC information", ErrPermissionDenied)
}

if permissions.IsSuperAdmin {
return &AdminAuthorizer{}, nil
return &SuperAdminAuthorizer{}, nil
}

organization, err := permissions.LookupOrganization(organizationName)
if err != nil {
return nil, err
}

if organization.IsAdmin {
return &AdminAuthorizer{}, nil
authorizer := &OrganizationAuthorizer{
permissions: organization,
}

return &OrganizationAuthorizer{
permissions: organization,
}, nil
return authorizer, nil
}
8 changes: 2 additions & 6 deletions pkg/authorization/rbac/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ import (

// Authorizer defines an interface for authorization.
type Authorizer interface {
// AllowedByRole allows access based on role, typically used for admin only interfaces.
AllowedByRole(role roles.Role) error
// AllowedByGroup allows access based on groups assigned to a restricted resource.
AllowedByGroup(groupIDs []string) error
// AllowedByGroupRole allows access baseed on groups and the role, typically for write access.
AllowedByGroupRole(groupIDs []string, role roles.Role) error
// Allow allows access based on API scope and required permissions.
Allow(scope string, permission roles.Permission) error
}
2 changes: 0 additions & 2 deletions pkg/authorization/rbac/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ type GroupPermissions struct {

// OrganizationPermissions are privilege grants for an organization.
type OrganizationPermissions struct {
// IsAdmin allows the user to play with all resources in an organization.
IsAdmin bool `json:"isAdmin,omitempty"`
// Name is the name of the organization.
Name string `json:"name"`
// Groups are any groups the user belongs to in an organization.
Expand Down
117 changes: 116 additions & 1 deletion pkg/authorization/roles/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ limitations under the License.

package roles

// Role defines the role a user has within the scope of a group.
import (
"errors"
"fmt"
)

var (
ErrRoleNotFound = errors.New("requested role not found")
)

// Role defines the role a user has within the Scope of a group.
// +kubebuilder:validation:Enum=superAdmin;admin;user;reader
type Role string

Expand All @@ -31,3 +40,109 @@ const (
// Readers have read-only access within allowed projects.
Reader Role = "reader"
)

type Permission string

const (
Create Permission = "create"
Read Permission = "read"
Update Permission = "update"
Delete Permission = "delete"
)

type Permissions []Permission

type ScopedPermissionsMap map[string]Permissions

type RoleInfo struct {
// Permissions defined what the role can do.
Permissions ScopedPermissionsMap
}

type RoleManager struct {
// mapper provides a quick lookup of role.
mapper map[Role]*RoleInfo
}

func New() *RoleManager {
reader := &RoleInfo{
Permissions: ScopedPermissionsMap{
"organization": Permissions{
Read,
},
"project": Permissions{
Read,
},
"infrastructure": Permissions{
Read,
},
},
}

user := &RoleInfo{
Permissions: ScopedPermissionsMap{
"organization": Permissions{
Read,
},
"project": Permissions{
Read,
},
"infrastructure": Permissions{
Create,
Read,
Update,
Delete,
},
},
}

// admin can do most things in an organization, except create and
// delete them as creation will require a separate verification and billing
// flow, deletion is too damned dangerous.
admin := &RoleInfo{
Permissions: ScopedPermissionsMap{
"organization": Permissions{
Read,
Update,
},
"oauth2provider:public": Permissions{
Read,
},
"oauth2provider:private": Permissions{
Create,
Read,
Update,
Delete,
},
"project": Permissions{
Create,
Read,
Update,
Delete,
},
"infrastructure": Permissions{
Create,
Read,
Update,
Delete,
},
},
}

return &RoleManager{
mapper: map[Role]*RoleInfo{
Reader: reader,
User: user,
Admin: admin,
},
}
}

func (m *RoleManager) GetRole(role Role) (*RoleInfo, error) {
roleInfo, ok := m.mapper[role]
if !ok {
return nil, fmt.Errorf("%w: %v", ErrRoleNotFound, role)
}

return roleInfo, nil
}
2 changes: 1 addition & 1 deletion pkg/authorization/userinfo/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ import (
func NewAuthorizer(ctx context.Context, organization string) (rbac.Authorizer, error) {
userinfo := FromContext(ctx)

return rbac.NewAuthorizer(userinfo.RBAC, organization)
return rbac.New(userinfo.RBAC, organization)
}

0 comments on commit f6a5ad6

Please sign in to comment.