Skip to content

Commit

Permalink
Shift RBAC to Identity
Browse files Browse the repository at this point in the history
  • Loading branch information
spjmurray committed Apr 4, 2024
1 parent 5a43261 commit 959782c
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 239 deletions.
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.20
appVersion: v0.1.20
version: v0.1.21
appVersion: v0.1.21

icon: https://assets.unikorn-cloud.org/images/logos/dark-on-light/icon.svg
33 changes: 33 additions & 0 deletions pkg/authorization/constants/roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright 2024 the Unikorn Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package constants

const (
// SuperAdmin users can do anything, anywhere, and should be
// restricted to platform operators only.
SuperAdmin = "superAdmin"
)

// +kubebuilder:validation:Enum=create;read;update;delete
type Permission string

const (
Create Permission = "create"
Read Permission = "read"
Update Permission = "update"
Delete Permission = "delete"
)
153 changes: 93 additions & 60 deletions pkg/authorization/rbac/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,111 +17,144 @@ limitations under the License.
package rbac

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"slices"

"github.com/unikorn-cloud/core/pkg/authorization/roles"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"

"github.com/unikorn-cloud/core/pkg/authorization/accesstoken"
"github.com/unikorn-cloud/core/pkg/authorization/constants"
)

var (
ErrPermissionDenied = errors.New("access denied")
)

// SuperAdminAuthorizer allows access to everything.
type SuperAdminAuthorizer struct{}
ErrRequestError = errors.New("request error")

func (a *SuperAdminAuthorizer) Allow(_ string, _ roles.Permission) error {
return nil
}
ErrCertError = errors.New("certificate error")
)

// ScopedAuthorizer is scoped to a specific organization.
type ScopedAuthorizer struct {
permissions *OrganizationPermissions
// IdentityACLGetter grabs an ACL for the user from the identity API.
// Used for any non-identity API.
type IdentityACLGetter struct {
host string
organization string
ca []byte
}

type PermissionMap map[roles.Permission]interface{}

type ScopedPermissionMap map[string]PermissionMap
func NewIdentityACLGetter(host, organization string) *IdentityACLGetter {
return &IdentityACLGetter{
host: host,
organization: organization,
}
}

func (a *ScopedAuthorizer) Allow(scope string, permission roles.Permission) error {
roleManager := roles.New()
func (a *IdentityACLGetter) WithCA(ca []byte) *IdentityACLGetter {
a.ca = ca

scopedPermissions := ScopedPermissionMap{}
return a
}

// Build up a set of scopes and permissions based on group membership and the roles
// associated with that.
for _, group := range a.permissions.Groups {
for _, r := range group.Roles {
rolePermissions, err := roleManager.GetRole(r)
if err != nil {
return err
}
func (a *IdentityACLGetter) Get(ctx context.Context) (*ACL, error) {
client := &http.Client{}

for roleScope, perms := range rolePermissions.Permissions {
if _, ok := scopedPermissions[roleScope]; !ok {
scopedPermissions[roleScope] = PermissionMap{}
}
// Handle things like let's encrypt staging.
if a.ca != nil {
certPool := x509.NewCertPool()

for _, perm := range perms {
scopedPermissions[roleScope][perm] = nil
}
}
if ok := certPool.AppendCertsFromPEM(a.ca); !ok {
return nil, fmt.Errorf("%w: unable to add CA certificate", ErrCertError)
}
}

s, ok := scopedPermissions[scope]
if !ok {
return fmt.Errorf("%w: not permitted to access the %v scope", ErrPermissionDenied, scope)
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
MinVersion: tls.VersionTLS13,
},
},
}
}

if _, ok := s[permission]; !ok {
return fmt.Errorf("%w: not permitted to %v within the %v scope", ErrPermissionDenied, permission, scope)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/organizations/%s/acl", a.host, a.organization), nil)
if err != nil {
return nil, err
}

return nil
}
req.Header.Set("Authorization", "bearer "+accesstoken.FromContext(ctx))
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))

func NewScoped(permissions *Permissions, organizationName string) (Authorizer, error) {
if permissions == nil {
return nil, fmt.Errorf("%w: user has no RBAC information", ErrPermissionDenied)
resp, err := client.Do(req)
if err != nil {
return nil, err
}

if permissions.IsSuperAdmin {
return &SuperAdminAuthorizer{}, nil
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: status code not as expected", ErrRequestError)
}

organization, err := permissions.LookupOrganization(organizationName)
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

authorizer := &ScopedAuthorizer{
permissions: organization,
acl := &ACL{}

if err := json.Unmarshal(body, &acl); err != nil {
return nil, err
}

return authorizer, nil
return acl, nil
}

// UncopedAuthorizer is not scoped to a specific organization.
type UnscopedAuthorizer struct {
// SuperAdminAuthorizer allows access to everything.
type SuperAdminAuthorizer struct{}

func (a *SuperAdminAuthorizer) Allow(_ context.Context, _ string, _ constants.Permission) error {
return nil
}

func (a *UnscopedAuthorizer) Allow(scope string, permission roles.Permission) error {
if scope == "organizations" && permission == roles.Read {
return nil
// BaseAuthorizer is scoped to a specific organization.
type BaseAuthorizer struct {
acl *ACL
}

func (a *BaseAuthorizer) Allow(ctx context.Context, scope string, permission constants.Permission) error {
aclScope := a.acl.GetScope(scope)
if aclScope == nil {
return fmt.Errorf("%w: not permitted access to the %v scope", ErrPermissionDenied, scope)
}

if !slices.Contains(aclScope.Permissions, permission) {
return fmt.Errorf("%w: not permitted %v access within the %v scope", ErrPermissionDenied, permission, scope)
}

return fmt.Errorf("%w: not permitted to %v within the %v scope", ErrPermissionDenied, permission, scope)
return nil
}

func NewUnscoped(permissions *Permissions) (Authorizer, error) {
if permissions == nil {
return nil, fmt.Errorf("%w: user has no RBAC information", ErrPermissionDenied)
func New(ctx context.Context, getter ACLGetter) (Authorizer, error) {
acl, err := getter.Get(ctx)
if err != nil {
return nil, err
}

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

return &UnscopedAuthorizer{}, nil
authorizer := &BaseAuthorizer{
acl: acl,
}

return authorizer, nil
}
26 changes: 26 additions & 0 deletions pkg/authorization/rbac/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,29 @@ func (p *Permissions) LookupOrganization(organization string) (*OrganizationPerm

return nil, ErrPermissionDenied
}

func (p *OrganizationPermissions) HasRole(name string) bool {
if p == nil {
return false
}

for _, group := range p.Groups {
for _, role := range group.Roles {
if role == name {
return true
}
}
}

return false
}

func (a *ACL) GetScope(name string) *Scope {
for _, scope := range a.Scopes {
if scope.Name == name {
return scope
}
}

return nil
}
11 changes: 9 additions & 2 deletions pkg/authorization/rbac/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ limitations under the License.
package rbac

import (
"github.com/unikorn-cloud/core/pkg/authorization/roles"
"context"

"github.com/unikorn-cloud/core/pkg/authorization/constants"
)

type ACLGetter interface {
// Get returns an ACL from whatever source the interface reprensents.
Get(ctx context.Context) (*ACL, error)
}

// Authorizer defines an interface for authorization.
type Authorizer interface {
// Allow allows access based on API scope and required permissions.
Allow(scope string, permission roles.Permission) error
Allow(ctx context.Context, scope string, permission constants.Permission) error
}
19 changes: 17 additions & 2 deletions pkg/authorization/rbac/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ limitations under the License.
package rbac

import (
"github.com/unikorn-cloud/core/pkg/authorization/roles"
"github.com/unikorn-cloud/core/pkg/authorization/constants"
)

// GroupPermissions are privilege grants for a project.
type GroupPermissions struct {
// ID is the unique, immutable project identifier.
ID string `json:"id"`
// Roles are the privileges a user has for the group.
Roles []roles.Role `json:"roles"`
Roles []string `json:"roles"`
}

// OrganizationPermissions are privilege grants for an organization.
Expand All @@ -43,3 +43,18 @@ type Permissions struct {
// Organizations are any organizations the user has access to.
Organizations []OrganizationPermissions `json:"organizations,omitempty"`
}

// Scope maps a named API scope to a set of permissions.
type Scope struct {
// Name is the name of the scope.
Name string `json:"name"`
// Permissions is the set of permissions allowed for that scope.
Permissions []constants.Permission `json:"permissions"`
}

// ACL maps scopes to permissions.
type ACL struct {
IsSuperAdmin bool `json:"isSuperAdmin,omitempty"`
// Scopes is the set of scoped APIs the role can access.
Scopes []*Scope `json:"scopes,omitempty"`
}
Loading

0 comments on commit 959782c

Please sign in to comment.