RBAC refactored (pkg renamed, init improved)
This commit is contained in:
19
pkg/rbac/effective.go
Normal file
19
pkg/rbac/effective.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package rbac
|
||||
|
||||
type (
|
||||
effective struct {
|
||||
Resource Resource `json:"resource"`
|
||||
Operation Operation `json:"operation"`
|
||||
Allow bool `json:"allow"`
|
||||
}
|
||||
|
||||
EffectiveSet []effective
|
||||
)
|
||||
|
||||
func (ee *EffectiveSet) Push(res Resource, op Operation, allow bool) {
|
||||
*ee = append(*ee, effective{
|
||||
Resource: res,
|
||||
Operation: op,
|
||||
Allow: allow,
|
||||
})
|
||||
}
|
||||
149
pkg/rbac/importer.go
Normal file
149
pkg/rbac/importer.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/deinterfacer"
|
||||
)
|
||||
|
||||
type (
|
||||
Importer struct {
|
||||
whitelist whitelistChecker
|
||||
|
||||
// Rules per role
|
||||
rules map[string]RuleSet
|
||||
}
|
||||
|
||||
whitelistChecker interface {
|
||||
Check(*Rule) bool
|
||||
}
|
||||
|
||||
ImportKeeper interface {
|
||||
Grant(ctx context.Context, rr ...*Rule) error
|
||||
}
|
||||
)
|
||||
|
||||
func NewImporter(wl whitelistChecker) *Importer {
|
||||
return &Importer{
|
||||
whitelist: wl,
|
||||
}
|
||||
}
|
||||
|
||||
// CastSet - resolves permission rules for specific resource:
|
||||
// <role>: [<operation>, ...]
|
||||
func (imp *Importer) CastSet(resource, accessStr string, roles interface{}) (err error) {
|
||||
if !deinterfacer.IsMap(roles) {
|
||||
return errors.New("expecting map of roles")
|
||||
}
|
||||
return deinterfacer.Each(roles, func(_ int, roleHandle string, oo interface{}) error {
|
||||
return imp.appendPermissionRule(roleHandle, accessStr, resource, oo)
|
||||
})
|
||||
}
|
||||
|
||||
// CastResourcesSet - resolves permission rules:
|
||||
// <role> { <resource>: [<operation>, ...] }
|
||||
func (imp *Importer) CastResourcesSet(accessStr string, roles interface{}) (err error) {
|
||||
// if !IsIterable(roles) {
|
||||
// return errors.New("expecting map of roles")
|
||||
// }
|
||||
|
||||
return deinterfacer.Each(roles, func(_ int, roleHandle string, perResource interface{}) error {
|
||||
if !deinterfacer.IsMap(perResource) {
|
||||
return errors.New("expecting map of resources")
|
||||
}
|
||||
|
||||
// Each resource
|
||||
return deinterfacer.Each(perResource, func(_ int, resource string, oo interface{}) error {
|
||||
// We want to make life of the person that's preparing the import data easy, so
|
||||
// let's do a little guessing instead of him:
|
||||
if strings.Contains(resource, ":") {
|
||||
// This is not service-level resource, trim * and : from the end
|
||||
resource = strings.TrimRight(resource, ":*")
|
||||
resource = Resource(resource + string(resourceDelimiter)).AppendWildcard().String()
|
||||
}
|
||||
|
||||
return imp.appendPermissionRule(roleHandle, accessStr, resource, oo)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (imp *Importer) appendPermissionRule(roleHandle, accessStr, res string, oo interface{}) (err error) {
|
||||
var access Access
|
||||
|
||||
if err = access.UnmarshalJSON([]byte(accessStr)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if imp.rules == nil {
|
||||
imp.rules = map[string]RuleSet{}
|
||||
}
|
||||
|
||||
if imp.rules[roleHandle] == nil {
|
||||
imp.rules[roleHandle] = RuleSet{}
|
||||
}
|
||||
|
||||
operations := deinterfacer.ToStrings(oo)
|
||||
if operations == nil {
|
||||
return errors.New("could not resolve permission rule operations")
|
||||
}
|
||||
|
||||
sort.Strings(operations)
|
||||
|
||||
for _, op := range operations {
|
||||
rule := &Rule{
|
||||
Access: access,
|
||||
Resource: Resource(res),
|
||||
Operation: Operation(op),
|
||||
}
|
||||
|
||||
if imp.whitelist != nil && !imp.whitelist.Check(rule) {
|
||||
return errors.Errorf("invalid rule: operation %q on resource %q", op, res)
|
||||
}
|
||||
|
||||
imp.rules[roleHandle] = append(imp.rules[roleHandle], rule)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateResources iterates over all rules and replaces resource (foo:bar => foo:42)
|
||||
func (imp *Importer) UpdateResources(base, handle string, ID uint64) {
|
||||
var (
|
||||
from = Resource(base).append(handle)
|
||||
to = Resource(base).AppendID(ID)
|
||||
)
|
||||
for _, rules := range imp.rules {
|
||||
for _, rule := range rules {
|
||||
if rule.Resource == from {
|
||||
rule.Resource = to
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (imp *Importer) UpdateRoles(handle string, ID uint64) {
|
||||
if imp.rules[handle] != nil {
|
||||
for _, rule := range imp.rules[handle] {
|
||||
rule.RoleID = ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (imp *Importer) Store(ctx context.Context, k ImportKeeper) (err error) {
|
||||
for _, rr := range imp.rules {
|
||||
// Make sure all rules have valid role
|
||||
rr, _ = rr.Filter(func(rule *Rule) (b bool, e error) {
|
||||
return rule.RoleID > 0, nil
|
||||
})
|
||||
|
||||
if err = k.Grant(ctx, rr...); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
48
pkg/rbac/importer_test.go
Normal file
48
pkg/rbac/importer_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPermissionRulesImport_CastResourcesSet(t *testing.T) {
|
||||
t.Skip()
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// resource string
|
||||
// yaml string
|
||||
// rules map[string]RuleSet
|
||||
// }{
|
||||
// {name: "empty", yaml: ``},
|
||||
// {name: "empty map", yaml: `{}`},
|
||||
// {name: "empty slice", yaml: `[]`},
|
||||
// {
|
||||
// name: "one role, one resource, two ops",
|
||||
// yaml: `admins: { resource: [ read, write ] }`,
|
||||
// rules: map[string]RuleSet{
|
||||
// "admins": {
|
||||
// AllowRule(0, "resource", "read"),
|
||||
// AllowRule(0, "resource", "write"),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: "op as string",
|
||||
// yaml: `admins: { resource: read }`,
|
||||
// rules: map[string]RuleSet{
|
||||
// "admins": {
|
||||
// AllowRule(0, "resource", "read"),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// for _, tt := range tests {
|
||||
// t.Run(tt.name, func(t *testing.T) {
|
||||
// imp := &Importer{}
|
||||
//
|
||||
// aux, err := importer.ParseYAML([]byte(tt.yaml))
|
||||
// require.NoError(t, err)
|
||||
// require.NoError(t, imp.CastResourcesSet("allow", aux))
|
||||
// require.Equal(t, tt.rules, imp.rules)
|
||||
// })
|
||||
// }
|
||||
}
|
||||
172
pkg/rbac/permissions.go
Normal file
172
pkg/rbac/permissions.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package rbac
|
||||
|
||||
// General permission stuff, types, constants
|
||||
|
||||
type (
|
||||
Operation string
|
||||
Access int
|
||||
|
||||
// CheckAccessFunc function.
|
||||
CheckAccessFunc func() Access
|
||||
|
||||
Whitelist struct {
|
||||
// Index is used for fast lookups
|
||||
index map[Resource]map[Operation]bool
|
||||
|
||||
// we need this to maintain a stable order of res/ops
|
||||
rules RuleSet
|
||||
}
|
||||
|
||||
whitelistFlatten struct {
|
||||
Resource `json:"resource"`
|
||||
Operation `json:"operation"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// EveryoneRoleID -- everyone
|
||||
EveryoneRoleID uint64 = 1
|
||||
|
||||
// AdminsRoleID - admins
|
||||
AdminsRoleID uint64 = 2
|
||||
|
||||
// OwnersDynamicRoleID for Owners role is dynamically assigned
|
||||
// when current user is owner of the resource
|
||||
OwnersDynamicRoleID uint64 = 10000
|
||||
|
||||
// CreatorsDynamicRoleID for Creators role is dynamically assigned
|
||||
// when current user created the resource
|
||||
CreatorsDynamicRoleID uint64 = 10010
|
||||
|
||||
// UpdatersDynamicRoleID for Updaters role is dynamically assigned
|
||||
// when current user updated the resource
|
||||
UpdatersDynamicRoleID uint64 = 10011
|
||||
|
||||
// DeletersDynamicRoleID for Deleters role is dynamically assigned
|
||||
// when current user deleted the resource
|
||||
DeletersDynamicRoleID uint64 = 10012
|
||||
|
||||
// MembersDynamicRoleID for Members role is dynamically assigned
|
||||
// when current user member of the resource
|
||||
// Can be used by resources that have members
|
||||
MembersDynamicRoleID uint64 = 10020
|
||||
|
||||
// AssigneesDynamicRoleID for Assignees role is dynamically assigned
|
||||
// when current user member of the resource
|
||||
// Can be used by resources that have assignees
|
||||
AssigneesDynamicRoleID uint64 = 10021
|
||||
)
|
||||
|
||||
func (op Operation) String() string {
|
||||
return string(op)
|
||||
}
|
||||
|
||||
func (a Access) String() string {
|
||||
switch a {
|
||||
case Allow:
|
||||
return "allow"
|
||||
case Deny:
|
||||
return "deny"
|
||||
default:
|
||||
return "inherit"
|
||||
}
|
||||
}
|
||||
|
||||
// Bool convers boolean true to Allow and false to Deny
|
||||
func BoolToCheckFunc(isTrue bool) CheckAccessFunc {
|
||||
return func() Access {
|
||||
if isTrue {
|
||||
return Allow
|
||||
}
|
||||
|
||||
return Deny
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Access) UnmarshalJSON(data []byte) error {
|
||||
switch string(data) {
|
||||
case "allow":
|
||||
*a = Allow
|
||||
case "deny":
|
||||
*a = Deny
|
||||
default:
|
||||
*a = Inherit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a Access) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + a.String() + `"`), nil
|
||||
}
|
||||
|
||||
func Allowed() Access {
|
||||
return Allow
|
||||
}
|
||||
|
||||
func Denied() Access {
|
||||
return Deny
|
||||
}
|
||||
|
||||
func (wl *Whitelist) Set(r Resource, oo ...Operation) {
|
||||
if wl.index == nil {
|
||||
wl.index = map[Resource]map[Operation]bool{}
|
||||
}
|
||||
|
||||
wl.index[r] = map[Operation]bool{}
|
||||
|
||||
for _, o := range oo {
|
||||
wl.index[r][o] = true
|
||||
wl.rules = append(wl.rules, &Rule{Resource: r, Operation: o})
|
||||
}
|
||||
}
|
||||
|
||||
func (wl Whitelist) Check(rule *Rule) bool {
|
||||
if rule == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
res := rule.Resource.TrimID()
|
||||
|
||||
if _, ok := wl.index[res]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return wl.index[res][rule.Operation]
|
||||
}
|
||||
|
||||
// Flatten casts list of operations for each resource from map to slice and creates more output friendly format
|
||||
func (wl Whitelist) Flatten() []whitelistFlatten {
|
||||
var (
|
||||
wlf = []whitelistFlatten{}
|
||||
)
|
||||
for _, r := range wl.rules {
|
||||
wlf = append(wlf, whitelistFlatten{r.Resource, r.Operation})
|
||||
}
|
||||
|
||||
return wlf
|
||||
}
|
||||
|
||||
// DynamicRoles is a utility function that compares
|
||||
// given u with each odd element in cc
|
||||
// and returns even element on a match
|
||||
//
|
||||
// In practice, pass userID as first argument and
|
||||
// set of userID-roleID pairs. Function returns
|
||||
// all roles that are paired with the same user
|
||||
func DynamicRoles(u uint64, cc ...uint64) (rr []uint64) {
|
||||
var l = len(cc)
|
||||
|
||||
if l%2 == 1 {
|
||||
panic("expecting even number of id/dynamic-role pairs")
|
||||
}
|
||||
|
||||
rr = make([]uint64, 0, l/2)
|
||||
|
||||
for i := 0; i < l; i += 2 {
|
||||
if cc[i] == u {
|
||||
rr = append(rr, cc[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
53
pkg/rbac/permissions_test.go
Normal file
53
pkg/rbac/permissions_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDynamicRoles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
u uint64
|
||||
cc []uint64
|
||||
exp []uint64
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
42,
|
||||
nil,
|
||||
[]uint64{},
|
||||
},
|
||||
{
|
||||
"only one",
|
||||
42,
|
||||
[]uint64{42, 2},
|
||||
[]uint64{2},
|
||||
},
|
||||
{
|
||||
"none",
|
||||
42,
|
||||
[]uint64{1, 2},
|
||||
[]uint64{},
|
||||
},
|
||||
{
|
||||
"few",
|
||||
42,
|
||||
[]uint64{42, 2, 43, 3},
|
||||
[]uint64{2},
|
||||
},
|
||||
{
|
||||
"all",
|
||||
42,
|
||||
[]uint64{42, 1, 42, 2},
|
||||
[]uint64{1, 2},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotRr := DynamicRoles(tt.u, tt.cc...); !reflect.DeepEqual(gotRr, tt.exp) {
|
||||
t.Errorf("DynamicRoles() = %v, want %v", gotRr, tt.exp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
83
pkg/rbac/resource.go
Normal file
83
pkg/rbac/resource.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
Resource string
|
||||
)
|
||||
|
||||
const (
|
||||
resourceDelimiter = ':'
|
||||
resourceWildcard = '*'
|
||||
)
|
||||
|
||||
func (r Resource) append(suffix string) Resource {
|
||||
if !r.IsAppendable() {
|
||||
panic("can not append to non appendable resource '" + r.String() + "'")
|
||||
}
|
||||
|
||||
return Resource(r.String() + suffix)
|
||||
}
|
||||
|
||||
// Resource to satisfty interfaces and ease development
|
||||
func (r Resource) RBACResource() Resource {
|
||||
return r
|
||||
}
|
||||
|
||||
// DynamicRoles satisfies Resourcable interface when Resource is
|
||||
// used directly
|
||||
func (r Resource) DynamicRoles(i uint64) []uint64 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Resource) AppendID(ID uint64) Resource {
|
||||
return r.append(strconv.FormatUint(ID, 10))
|
||||
}
|
||||
|
||||
func (r Resource) AppendWildcard() Resource {
|
||||
return r.TrimID().append(string(resourceWildcard))
|
||||
}
|
||||
|
||||
// Trims off wildcard/id from resource
|
||||
func (r Resource) TrimID() Resource {
|
||||
s := r.String()
|
||||
p := strings.LastIndexByte(s, resourceDelimiter)
|
||||
if p > 0 {
|
||||
return Resource(s[0 : p+1])
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// IsAppendable checks if Resource has trailing resource delimiter
|
||||
func (r Resource) IsAppendable() bool {
|
||||
return strings.IndexByte(r.String(), resourceDelimiter) > -1
|
||||
}
|
||||
|
||||
// IsValid does basic resource validation
|
||||
func (r Resource) IsValid() bool {
|
||||
return len(r) > 0 && r[len(r)-1] != resourceDelimiter
|
||||
}
|
||||
|
||||
// IsServiceLevel checks for resource delimiters - service level resources do not have it
|
||||
func (r Resource) GetService() Resource {
|
||||
s := r.String()
|
||||
p := strings.IndexByte(s, resourceDelimiter)
|
||||
if p > 0 {
|
||||
return Resource(s[0:p])
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// HasWildcard checks if resource has wildcard char at the end
|
||||
func (r Resource) HasWildcard() bool {
|
||||
return len(r) > 0 && r[len(r)-1] == resourceWildcard
|
||||
}
|
||||
|
||||
func (r Resource) String() string {
|
||||
return string(r)
|
||||
}
|
||||
63
pkg/rbac/resource_test.go
Normal file
63
pkg/rbac/resource_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResource(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
|
||||
sCases = []struct {
|
||||
r Resource
|
||||
s string
|
||||
}{
|
||||
{
|
||||
Resource("a:b:c"),
|
||||
"a:b:c"},
|
||||
{
|
||||
Resource("a:b:c").RBACResource(),
|
||||
"a:b:c"},
|
||||
{
|
||||
Resource("a:b:").AppendID(1),
|
||||
"a:b:1"},
|
||||
{
|
||||
Resource("a:b:").AppendWildcard(),
|
||||
"a:b:*"},
|
||||
{
|
||||
Resource("a:b:1").TrimID(),
|
||||
"a:b:"},
|
||||
{
|
||||
Resource("a:b:1").GetService(),
|
||||
"a"},
|
||||
}
|
||||
)
|
||||
|
||||
for _, sc := range sCases {
|
||||
req.Equal(sc.s, sc.r.String())
|
||||
}
|
||||
|
||||
var r string
|
||||
r = "a:"
|
||||
req.True(Resource(r).IsAppendable(), "Expecting resource %q to be appendable", r)
|
||||
r = "a:1"
|
||||
req.True(Resource(r).IsAppendable(), "Expecting resource %q to be appendable", r)
|
||||
r = "a:*"
|
||||
req.True(Resource(r).IsAppendable(), "Expecting resource %q to be appendable", r)
|
||||
|
||||
r = "a"
|
||||
req.True(Resource(r).IsValid(), "Expecting resource %q to be valid", r)
|
||||
r = "a:"
|
||||
req.False(Resource(r).IsValid(), "Expecting resource %q not to be valid", r)
|
||||
r = "a:1"
|
||||
req.True(Resource(r).IsValid(), "Expecting resource %q to be valid", r)
|
||||
r = "a:*"
|
||||
req.True(Resource(r).IsValid(), "Expecting resource %q to be valid", r)
|
||||
|
||||
r = "a:1"
|
||||
req.False(Resource(r).HasWildcard(), "Expecting resource %q to not have wildcard", r)
|
||||
r = "a:*"
|
||||
req.True(Resource(r).HasWildcard(), "Expecting resource %q to have wildcard", r)
|
||||
}
|
||||
57
pkg/rbac/rule.go
Normal file
57
pkg/rbac/rule.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
Rule struct {
|
||||
RoleID uint64 `json:"roleID,string"`
|
||||
Resource Resource `json:"resource"`
|
||||
Operation Operation `json:"operation"`
|
||||
Access Access `json:"access,string"`
|
||||
|
||||
// Do we need to flush it to storage?
|
||||
dirty bool
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// Allow - Operation over a resource is allowed
|
||||
Allow Access = 1
|
||||
|
||||
// Deny - Operation over a resource is denied
|
||||
Deny = 0
|
||||
|
||||
// Inherit - Operation over a resource is not defined, inherit
|
||||
Inherit = -1
|
||||
)
|
||||
|
||||
func (r Rule) String() string {
|
||||
return fmt.Sprintf("%s %d to %s on %s", r.Access, r.RoleID, r.Operation, r.Resource)
|
||||
}
|
||||
|
||||
func (r Rule) Equals(cmp *Rule) bool {
|
||||
if cmp == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return r.RoleID == cmp.RoleID &&
|
||||
r.Resource == cmp.Resource &&
|
||||
r.Operation == cmp.Operation
|
||||
}
|
||||
|
||||
// AllowRule helper func to create allow rule
|
||||
func AllowRule(id uint64, r Resource, o Operation) *Rule {
|
||||
return &Rule{id, r, o, Allow, false}
|
||||
}
|
||||
|
||||
// DenyRule helper func to create deny rule
|
||||
func DenyRule(id uint64, r Resource, o Operation) *Rule {
|
||||
return &Rule{id, r, o, Deny, false}
|
||||
}
|
||||
|
||||
// InheritRule helper func to create inherit rule
|
||||
func InheritRule(id uint64, r Resource, o Operation) *Rule {
|
||||
return &Rule{id, r, o, Inherit, false}
|
||||
}
|
||||
86
pkg/rbac/ruleset_checks.go
Normal file
86
pkg/rbac/ruleset_checks.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package rbac
|
||||
|
||||
// Check verifies if role has access to perform an operation on a resource
|
||||
//
|
||||
// Overall flow:
|
||||
// - invalid resource, no access
|
||||
// - can this combination of roles perform an operation on this specific resource
|
||||
// - can this combination of roles perform an operation on any resource of the type (wildcard)
|
||||
// - can anyone/everyone perform an operation on this specific resource
|
||||
// - can anyone/everyone perform an operation on any resource of the type (wildcard)
|
||||
func (set RuleSet) Check(res Resource, op Operation, roles ...uint64) (v Access) {
|
||||
|
||||
if !res.IsValid() {
|
||||
return Deny
|
||||
}
|
||||
|
||||
if len(roles) > 0 {
|
||||
if v = set.checkResource(res, op, roles...); v != Inherit {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if v = set.checkResource(res, op, EveryoneRoleID); v != Inherit {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Check ability to perform an operation on a specific and wildcard resource
|
||||
func (set RuleSet) checkResource(res Resource, op Operation, roles ...uint64) (v Access) {
|
||||
if v = set.check(res, op, roles...); v != Inherit {
|
||||
return
|
||||
}
|
||||
|
||||
if res.IsAppendable() {
|
||||
// Is this a specific resource and can we turn it into a wild-carded resource?
|
||||
if v = set.check(res.AppendWildcard(), op, roles...); v != Inherit {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Check verifies if any of given roles has permission to perform an operation over a resource
|
||||
//
|
||||
// Will return Inherit when:
|
||||
// - no roles are given
|
||||
// - more than 1 role is given and one of the given roles is Everyone
|
||||
//
|
||||
// Will return Deny when:
|
||||
// - there is one rule with Deny value
|
||||
//
|
||||
// Will return Allow when:
|
||||
// - there is at least one rule with Allow value (and no Deny rules)
|
||||
func (set RuleSet) check(res Resource, op Operation, roles ...uint64) (v Access) {
|
||||
v = Inherit
|
||||
|
||||
for i := range set {
|
||||
// Ignore resources & operations that do not match
|
||||
if set[i].Resource != res || set[i].Operation != op {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for every role
|
||||
for _, roleID := range roles {
|
||||
// Skip rules that do not match
|
||||
if set[i].RoleID != roleID || set[i].Access == Inherit {
|
||||
continue
|
||||
}
|
||||
|
||||
v = set[i].Access // set to Allow
|
||||
|
||||
// Return on first Deny
|
||||
if v == Deny {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the rules matched, return Inherit (see 1st line)
|
||||
// if at least one of the rules allowed this op over a resource,
|
||||
// return Allow.
|
||||
return v
|
||||
}
|
||||
187
pkg/rbac/ruleset_checks_test.go
Normal file
187
pkg/rbac/ruleset_checks_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
role1 uint64 = 10001
|
||||
role2 uint64 = 10002
|
||||
|
||||
resService1 = Resource("service1")
|
||||
resService2 = Resource("service2")
|
||||
|
||||
resThingWc = Resource("some:answer:*")
|
||||
resThing13 = Resource("some:answer:13")
|
||||
resThing42 = Resource("some:answer:42")
|
||||
|
||||
opAccess = "access"
|
||||
opRead = "read"
|
||||
opWrite = "write"
|
||||
)
|
||||
|
||||
func TestRuleSet_check(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
|
||||
rr = RuleSet{
|
||||
AllowRule(role1, resThing42, opRead),
|
||||
DenyRule(role1, resThing13, opWrite),
|
||||
AllowRule(role2, resThing13, opWrite),
|
||||
}
|
||||
|
||||
sCases = []struct {
|
||||
roles []uint64
|
||||
res Resource
|
||||
op Operation
|
||||
expected Access
|
||||
}{
|
||||
{[]uint64{role1}, resThing42, opRead, Allow},
|
||||
{[]uint64{role1}, resThing42, opWrite, Inherit},
|
||||
{[]uint64{role1}, resThing13, opWrite, Deny},
|
||||
{[]uint64{role2}, resThing13, opWrite, Allow},
|
||||
{[]uint64{role1, role2}, resThing13, opWrite, Deny},
|
||||
{[]uint64{role1, role2}, resThing42, opRead, Allow},
|
||||
}
|
||||
)
|
||||
|
||||
for c, sc := range sCases {
|
||||
v := rr.check(sc.res, sc.op, sc.roles...)
|
||||
req.Equalf(sc.expected, v, "Check test #%d failed, expected %s, got %s", c, sc.expected, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Test resource inheritance
|
||||
func TestRuleSet_checkResource(t *testing.T) {
|
||||
const (
|
||||
role1 uint64 = 10001
|
||||
|
||||
resService1 = Resource("service1")
|
||||
resService2 = Resource("service2")
|
||||
|
||||
resThingWc = Resource("some:answer:*")
|
||||
resThing13 = Resource("some:answer:13")
|
||||
resThing42 = Resource("some:answer:42")
|
||||
|
||||
opAccess = "access"
|
||||
)
|
||||
|
||||
var (
|
||||
r = require.New(t)
|
||||
|
||||
sCases = []struct {
|
||||
rr RuleSet
|
||||
roles []uint64
|
||||
res Resource
|
||||
op Operation
|
||||
expected Access
|
||||
}{
|
||||
{
|
||||
RuleSet{
|
||||
AllowRule(role1, resService1, opAccess),
|
||||
},
|
||||
[]uint64{role1},
|
||||
resService1,
|
||||
opAccess,
|
||||
Allow,
|
||||
},
|
||||
{
|
||||
RuleSet{
|
||||
AllowRule(role1, resThingWc, opAccess),
|
||||
},
|
||||
[]uint64{role1},
|
||||
resThing42,
|
||||
opAccess,
|
||||
Allow,
|
||||
},
|
||||
{ // deny wc and explictly allow 42
|
||||
RuleSet{
|
||||
DenyRule(role1, resThingWc, opAccess),
|
||||
AllowRule(role1, resThing42, opAccess),
|
||||
},
|
||||
[]uint64{role1},
|
||||
resThing42,
|
||||
opAccess,
|
||||
Allow,
|
||||
},
|
||||
{ // deny wc and explictly allow 42
|
||||
RuleSet{
|
||||
DenyRule(role1, resThingWc, opAccess),
|
||||
AllowRule(role1, resThing42, opAccess),
|
||||
},
|
||||
[]uint64{role1},
|
||||
resThing13,
|
||||
opAccess,
|
||||
Deny,
|
||||
},
|
||||
{ // deny wc and and check if wc is denied
|
||||
RuleSet{
|
||||
DenyRule(role1, resThingWc, opAccess),
|
||||
AllowRule(role1, resThing42, opAccess),
|
||||
},
|
||||
[]uint64{role1},
|
||||
resThingWc,
|
||||
opAccess,
|
||||
Deny,
|
||||
},
|
||||
{ // allow wc and and check if wc is allowed
|
||||
RuleSet{
|
||||
AllowRule(role1, resThingWc, opAccess),
|
||||
DenyRule(role1, resThing42, opAccess),
|
||||
},
|
||||
[]uint64{role1},
|
||||
resThingWc,
|
||||
opAccess,
|
||||
Allow,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for c, sc := range sCases {
|
||||
v := sc.rr.checkResource(sc.res, sc.op, sc.roles...)
|
||||
r.Equalf(sc.expected, v, "Check test #%d failed, expected %s, got %s", c, sc.expected, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Test role inheritance
|
||||
func TestRuleSet_Check(t *testing.T) {
|
||||
var (
|
||||
rr = RuleSet{
|
||||
// 1st level
|
||||
AllowRule(role1, resService1, opAccess),
|
||||
DenyRule(role2, resService1, opAccess),
|
||||
// 2nd level
|
||||
DenyRule(EveryoneRoleID, resService2, opAccess),
|
||||
AllowRule(EveryoneRoleID, resThing13, opAccess),
|
||||
AllowRule(role1, resService2, opAccess),
|
||||
// 3rd level
|
||||
DenyRule(EveryoneRoleID, resThingWc, opAccess),
|
||||
AllowRule(role1, resThing42, opAccess),
|
||||
}
|
||||
|
||||
r = require.New(t)
|
||||
|
||||
sCases = []struct {
|
||||
roles []uint64
|
||||
res Resource
|
||||
op Operation
|
||||
expected Access
|
||||
}{
|
||||
{[]uint64{role1}, resService1, opAccess, Allow},
|
||||
{[]uint64{role2}, resService1, opAccess, Deny},
|
||||
{[]uint64{role1}, resService2, opAccess, Allow},
|
||||
{[]uint64{role2}, resService2, opAccess, Deny},
|
||||
{[]uint64{role1}, resThing42, opAccess, Allow},
|
||||
{[]uint64{role2}, resThing42, opAccess, Deny},
|
||||
{[]uint64{}, resThing42, opAccess, Deny},
|
||||
{[]uint64{}, resThing13, opAccess, Allow},
|
||||
}
|
||||
)
|
||||
|
||||
for c, sc := range sCases {
|
||||
v := rr.Check(sc.res, sc.op, sc.roles...)
|
||||
r.Equalf(sc.expected, v, "Check test #%d failed, expected %s, got %s", c, sc.expected, v)
|
||||
}
|
||||
}
|
||||
130
pkg/rbac/ruleset_utils.go
Normal file
130
pkg/rbac/ruleset_utils.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package rbac
|
||||
|
||||
import "github.com/cortezaproject/corteza-server/pkg/slice"
|
||||
|
||||
// merge applies new rules (changes) to existing set and mark all changes as dirty
|
||||
func (set RuleSet) merge(rules ...*Rule) (out RuleSet) {
|
||||
var (
|
||||
o int
|
||||
olen = len(set)
|
||||
)
|
||||
|
||||
if olen == 0 {
|
||||
// Nothing exists yet, mark all as dirty
|
||||
for r := range rules {
|
||||
rules[r].dirty = true
|
||||
}
|
||||
|
||||
return rules
|
||||
} else {
|
||||
out = set
|
||||
|
||||
newRules:
|
||||
for _, rule := range rules {
|
||||
// Never go beyond the last old rule (olen)
|
||||
for o = 0; o < olen; o++ {
|
||||
if out[o].Equals(rule) {
|
||||
out[o].dirty = out[o].Access != rule.Access
|
||||
out[o].Access = rule.Access
|
||||
|
||||
// only one rule can match so proceed with next new rule
|
||||
continue newRules
|
||||
}
|
||||
}
|
||||
|
||||
// none of the old rules matched, append
|
||||
var c = *rule
|
||||
c.dirty = true
|
||||
|
||||
out = append(out, &c)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// dirty returns list of changed (dirty==true) and deleted (Access==Inherit) rules
|
||||
func (set RuleSet) dirty() (inherited, rest RuleSet) {
|
||||
inherited, rest = RuleSet{}, RuleSet{}
|
||||
|
||||
for _, r := range set {
|
||||
var c = *r
|
||||
if r.Access == Inherit {
|
||||
inherited = append(inherited, &c)
|
||||
} else if r.dirty {
|
||||
rest = append(rest, &c)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// reset dirty flag
|
||||
func (set RuleSet) clear() {
|
||||
_ = set.Walk(func(rule *Rule) error {
|
||||
rule.dirty = false
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Missing compares cmp with existing set
|
||||
// and returns rules that exists in set but not in cmp
|
||||
func (set RuleSet) Diff(cmp RuleSet) RuleSet {
|
||||
diff := RuleSet{}
|
||||
base:
|
||||
for _, s := range set {
|
||||
for _, c := range cmp {
|
||||
if c.Equals(s) {
|
||||
continue base
|
||||
}
|
||||
}
|
||||
|
||||
diff = append(diff, s)
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
// Roles returns list of unique id of all roles in the rule set
|
||||
func (set RuleSet) Roles() []uint64 {
|
||||
roles := make([]uint64, 0)
|
||||
for _, r := range set {
|
||||
if slice.HasUint64(roles, r.RoleID) {
|
||||
continue
|
||||
}
|
||||
|
||||
roles = append(roles, r.RoleID)
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
func (set RuleSet) ByResource(res Resource) RuleSet {
|
||||
out, _ := set.Filter(func(r *Rule) (bool, error) {
|
||||
return res == r.Resource, nil
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (set RuleSet) AllAllows() RuleSet {
|
||||
return set.ByAccess(Allow)
|
||||
}
|
||||
|
||||
func (set RuleSet) AllDenies() RuleSet {
|
||||
return set.ByAccess(Deny)
|
||||
}
|
||||
|
||||
func (set RuleSet) ByAccess(a Access) RuleSet {
|
||||
out, _ := set.Filter(func(r *Rule) (bool, error) {
|
||||
return a == r.Access, nil
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (set RuleSet) ByRole(roleID uint64) RuleSet {
|
||||
out, _ := set.Filter(func(r *Rule) (bool, error) {
|
||||
return roleID == r.RoleID, nil
|
||||
})
|
||||
return out
|
||||
}
|
||||
87
pkg/rbac/ruleset_utils_test.go
Normal file
87
pkg/rbac/ruleset_utils_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test role inheritance
|
||||
func TestRuleSet_merge(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
|
||||
sCases = []struct {
|
||||
old RuleSet
|
||||
new RuleSet
|
||||
del RuleSet
|
||||
upd RuleSet
|
||||
}{
|
||||
{
|
||||
RuleSet{AllowRule(role1, resService1, opAccess)},
|
||||
RuleSet{AllowRule(role1, resService1, opAccess)},
|
||||
RuleSet{},
|
||||
RuleSet{},
|
||||
},
|
||||
{
|
||||
RuleSet{AllowRule(role1, resService1, opAccess)},
|
||||
RuleSet{DenyRule(role1, resService1, opAccess)},
|
||||
RuleSet{},
|
||||
RuleSet{DenyRule(role1, resService1, opAccess)},
|
||||
},
|
||||
{
|
||||
RuleSet{AllowRule(role1, resService1, opAccess)},
|
||||
RuleSet{InheritRule(role1, resService1, opAccess)},
|
||||
RuleSet{InheritRule(role1, resService1, opAccess)},
|
||||
RuleSet{},
|
||||
},
|
||||
{
|
||||
RuleSet{AllowRule(role1, resService1, opAccess)},
|
||||
RuleSet{AllowRule(role1, resService1, opAccess)},
|
||||
RuleSet{},
|
||||
RuleSet{},
|
||||
},
|
||||
{
|
||||
RuleSet{
|
||||
AllowRule(role1, resService1, opAccess),
|
||||
DenyRule(role2, resService1, opAccess),
|
||||
DenyRule(EveryoneRoleID, resService2, opAccess),
|
||||
AllowRule(role1, resService2, opAccess),
|
||||
AllowRule(role2, resThing42, opAccess),
|
||||
},
|
||||
RuleSet{
|
||||
DenyRule(EveryoneRoleID, resThingWc, opAccess),
|
||||
AllowRule(role1, resService2, opAccess),
|
||||
AllowRule(role1, resThing42, opAccess),
|
||||
InheritRule(role2, resThing42, opAccess),
|
||||
},
|
||||
RuleSet{
|
||||
InheritRule(role2, resThing42, opAccess),
|
||||
},
|
||||
RuleSet{
|
||||
// AllowRule(role1, resService1, opAccess),
|
||||
// DenyRule(role2, resService1, opAccess),
|
||||
// DenyRule(EveryoneRoleID, resService2, opAccess),
|
||||
// AllowRule(role1, resService2, opAccess),
|
||||
DenyRule(EveryoneRoleID, resThingWc, opAccess),
|
||||
AllowRule(role1, resThing42, opAccess),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, sc := range sCases {
|
||||
// Apply changed and get update candidates
|
||||
mrg := sc.old.merge(sc.new...)
|
||||
del, upd := mrg.dirty()
|
||||
|
||||
// Clear dirty flag so that we do not confuse DeepEqual
|
||||
del.clear()
|
||||
upd.clear()
|
||||
|
||||
req.Equal(len(sc.del), len(del))
|
||||
req.Equal(len(sc.upd), len(upd))
|
||||
req.Equal(sc.del, del)
|
||||
req.Equal(sc.upd, upd)
|
||||
}
|
||||
}
|
||||
227
pkg/rbac/service.go
Normal file
227
pkg/rbac/service.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza-server/pkg/sentry"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
service struct {
|
||||
l *sync.Mutex
|
||||
logger *zap.Logger
|
||||
|
||||
// service will flush values on TRUE or just reload on FALSE
|
||||
f chan bool
|
||||
|
||||
rules RuleSet
|
||||
|
||||
store rbacRulesStore
|
||||
}
|
||||
|
||||
// RuleFilter is a dummy struct to satisfy store codegen
|
||||
RuleFilter struct{}
|
||||
|
||||
Controller interface {
|
||||
Can(roles []uint64, res Resource, op Operation, ff ...CheckAccessFunc) bool
|
||||
Check(res Resource, op Operation, roles ...uint64) (v Access)
|
||||
Grant(ctx context.Context, wl Whitelist, rules ...*Rule) (err error)
|
||||
Watch(ctx context.Context)
|
||||
FindRulesByRoleID(roleID uint64) (rr RuleSet)
|
||||
Rules() (rr RuleSet)
|
||||
Reload(ctx context.Context)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// Global RBAC service
|
||||
gRBAC Controller
|
||||
)
|
||||
|
||||
const (
|
||||
watchInterval = time.Hour
|
||||
)
|
||||
|
||||
// Global returns global RBAC service
|
||||
func Global() Controller {
|
||||
return gRBAC
|
||||
}
|
||||
|
||||
func SetGlobal(svc Controller) {
|
||||
gRBAC = svc
|
||||
}
|
||||
|
||||
func Initialize(logger *zap.Logger, s rbacRulesStore) error {
|
||||
if gRBAC != nil {
|
||||
// Prevent multiple initializations
|
||||
return nil
|
||||
}
|
||||
|
||||
SetGlobal(NewService(logger, s))
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewService initializes service{} struct
|
||||
//
|
||||
// service{} struct preloads, checks, grants and flushes privileges to and from store
|
||||
// It acts as a caching layer
|
||||
func NewService(logger *zap.Logger, s rbacRulesStore) (svc *service) {
|
||||
svc = &service{
|
||||
l: &sync.Mutex{},
|
||||
f: make(chan bool),
|
||||
|
||||
logger: logger.Named("rbac"),
|
||||
store: s,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Can function performs permission check for roles in context
|
||||
//
|
||||
// First extracts roles from context, then
|
||||
// use Check() to test against permission rules and
|
||||
// iterate over all fallback functions
|
||||
//
|
||||
// System user is always allowed to do everything
|
||||
//
|
||||
// When not explicitly allowed through rules or fallbacks, function will return FALSE.
|
||||
func (svc service) Can(roles []uint64, res Resource, op Operation, ff ...CheckAccessFunc) bool {
|
||||
// Checking rules
|
||||
var v = svc.Check(res.RBACResource(), op, roles...)
|
||||
if v != Inherit {
|
||||
return v == Allow
|
||||
}
|
||||
|
||||
// Checking fallback functions
|
||||
for _, f := range ff {
|
||||
v = f()
|
||||
|
||||
if v != Inherit {
|
||||
return v == Allow
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Check verifies if role has access to perform an operation on a resource
|
||||
//
|
||||
// See RuleSet's Check() func for details
|
||||
func (svc service) Check(res Resource, op Operation, roles ...uint64) (v Access) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
return svc.rules.Check(res, op, roles...)
|
||||
}
|
||||
|
||||
// Grant appends and/or overwrites internal rules slice
|
||||
//
|
||||
// All rules with Inherit are removed
|
||||
func (svc *service) Grant(ctx context.Context, wl Whitelist, rules ...*Rule) (err error) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
if err = svc.checkRules(wl, rules...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.grant(rules...)
|
||||
|
||||
return svc.flush(ctx)
|
||||
}
|
||||
|
||||
func (svc service) checkRules(wl Whitelist, rules ...*Rule) error {
|
||||
for _, r := range rules {
|
||||
if !wl.Check(r) {
|
||||
return errors.Errorf("invalid rule: '%s' on '%s'", r.Operation, r.Resource)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *service) grant(rules ...*Rule) {
|
||||
svc.rules = svc.rules.merge(rules...)
|
||||
}
|
||||
|
||||
// Watches for changes
|
||||
func (svc service) Watch(ctx context.Context) {
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
var ticker = time.NewTicker(watchInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
svc.Reload(ctx)
|
||||
case <-svc.f:
|
||||
svc.Reload(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
svc.logger.Debug("watcher initialized")
|
||||
}
|
||||
|
||||
func (svc service) FindRulesByRoleID(roleID uint64) (rr RuleSet) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
rr, _ = svc.rules.Filter(func(rule *Rule) (b bool, e error) {
|
||||
return rule.RoleID == roleID, nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc service) Rules() (rr RuleSet) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
return svc.rules
|
||||
}
|
||||
|
||||
func (svc *service) Reload(ctx context.Context) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
rr, _, err := svc.store.SearchRbacRules(ctx, RuleFilter{})
|
||||
svc.logger.Debug(
|
||||
"reloading rules",
|
||||
zap.Error(err),
|
||||
zap.Int("before", len(svc.rules)),
|
||||
zap.Int("after", len(rr)),
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
svc.rules = rr
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) flush(ctx context.Context) (err error) {
|
||||
d, u := svc.rules.dirty()
|
||||
|
||||
err = svc.store.DeleteRbacRule(ctx, d...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = svc.store.UpsertRbacRule(ctx, u...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.clear()
|
||||
svc.rules = u
|
||||
svc.logger.Debug("flushed rules",
|
||||
zap.Int("updated", len(u)),
|
||||
zap.Int("deleted", len(d)))
|
||||
|
||||
return
|
||||
}
|
||||
83
pkg/rbac/service_alt.go
Normal file
83
pkg/rbac/service_alt.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
ServiceAllowAll struct{}
|
||||
ServiceDenyAll struct{}
|
||||
TestService struct {
|
||||
service
|
||||
}
|
||||
)
|
||||
|
||||
func (ServiceAllowAll) Can([]uint64, Resource, Operation, ...CheckAccessFunc) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ServiceAllowAll) Check(Resource, Operation, ...uint64) (v Access) {
|
||||
return Allow
|
||||
}
|
||||
|
||||
func (ServiceAllowAll) Grant(context.Context, Whitelist, ...*Rule) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ServiceAllowAll) FindRulesByRoleID(roleID uint64) (rr RuleSet) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ServiceDenyAll) Can([]uint64, Resource, Operation, ...CheckAccessFunc) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ServiceDenyAll) Check(Resource, Operation, ...uint64) (v Access) {
|
||||
return Deny
|
||||
}
|
||||
|
||||
func (ServiceDenyAll) Grant(context.Context, Whitelist, ...*Rule) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ServiceDenyAll) FindRulesByRoleID(uint64) (rr RuleSet) {
|
||||
return
|
||||
}
|
||||
|
||||
func (svc *TestService) ClearGrants() {
|
||||
_ = svc.store.TruncateRbacRules(context.Background())
|
||||
svc.rules = RuleSet{}
|
||||
}
|
||||
|
||||
func (svc *TestService) String() (out string) {
|
||||
tpl := "%20v\t%-30s\t%-30s\t%v\n"
|
||||
out = fmt.Sprintf(tpl, "role", "res", "op", "access")
|
||||
out += strings.Repeat("-", 120) + "\n"
|
||||
|
||||
_ = svc.rules.Walk(func(r *Rule) error {
|
||||
out += fmt.Sprintf(tpl, r.RoleID, r.Resource, r.Operation, r.Access)
|
||||
return nil
|
||||
})
|
||||
|
||||
out += strings.Repeat("-", 120) + "\n"
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func NewTestService(logger *zap.Logger, s rbacRulesStore) (svc *TestService) {
|
||||
svc = &TestService{
|
||||
service: service{
|
||||
l: &sync.Mutex{},
|
||||
f: make(chan bool),
|
||||
|
||||
logger: logger.Named("rbac-test"),
|
||||
store: s,
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
15
pkg/rbac/store_interface.go
Normal file
15
pkg/rbac/store_interface.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type (
|
||||
// All
|
||||
rbacRulesStore interface {
|
||||
SearchRbacRules(ctx context.Context, f RuleFilter) (RuleSet, RuleFilter, error)
|
||||
UpsertRbacRule(ctx context.Context, rr ...*Rule) error
|
||||
DeleteRbacRule(ctx context.Context, rr ...*Rule) error
|
||||
TruncateRbacRules(ctx context.Context) error
|
||||
}
|
||||
)
|
||||
82
pkg/rbac/type_set.gen.go
Normal file
82
pkg/rbac/type_set.gen.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package rbac
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// pkg/rbac/types.yaml
|
||||
|
||||
type (
|
||||
|
||||
// ResourceSet slice of Resource
|
||||
//
|
||||
// This type is auto-generated.
|
||||
ResourceSet []*Resource
|
||||
|
||||
// RuleSet slice of Rule
|
||||
//
|
||||
// This type is auto-generated.
|
||||
RuleSet []*Rule
|
||||
)
|
||||
|
||||
// Walk iterates through every slice item and calls w(Resource) err
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ResourceSet) Walk(w func(*Resource) error) (err error) {
|
||||
for i := range set {
|
||||
if err = w(set[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filter iterates through every slice item, calls f(Resource) (bool, err) and return filtered slice
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ResourceSet) Filter(f func(*Resource) (bool, error)) (out ResourceSet, err error) {
|
||||
var ok bool
|
||||
out = ResourceSet{}
|
||||
for i := range set {
|
||||
if ok, err = f(set[i]); err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
out = append(out, set[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Walk iterates through every slice item and calls w(Rule) err
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set RuleSet) Walk(w func(*Rule) error) (err error) {
|
||||
for i := range set {
|
||||
if err = w(set[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filter iterates through every slice item, calls f(Rule) (bool, err) and return filtered slice
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set RuleSet) Filter(f func(*Rule) (bool, error)) (out RuleSet, err error) {
|
||||
var ok bool
|
||||
out = RuleSet{}
|
||||
for i := range set {
|
||||
if ok, err = f(set[i]); err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
out = append(out, set[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
127
pkg/rbac/type_set.gen_test.go
Normal file
127
pkg/rbac/type_set.gen_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package rbac
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// pkg/rbac/types.yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceSetWalk(t *testing.T) {
|
||||
var (
|
||||
value = make(ResourceSet, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// check walk with no errors
|
||||
{
|
||||
err := value.Walk(func(*Resource) error {
|
||||
return nil
|
||||
})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
// check walk with error
|
||||
req.Error(value.Walk(func(*Resource) error { return fmt.Errorf("walk error") }))
|
||||
}
|
||||
|
||||
func TestResourceSetFilter(t *testing.T) {
|
||||
var (
|
||||
value = make(ResourceSet, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// filter nothing
|
||||
{
|
||||
set, err := value.Filter(func(*Resource) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
req.NoError(err)
|
||||
req.Equal(len(set), len(value))
|
||||
}
|
||||
|
||||
// filter one item
|
||||
{
|
||||
found := false
|
||||
set, err := value.Filter(func(*Resource) (bool, error) {
|
||||
if !found {
|
||||
found = true
|
||||
return found, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
req.NoError(err)
|
||||
req.Len(set, 1)
|
||||
}
|
||||
|
||||
// filter error
|
||||
{
|
||||
_, err := value.Filter(func(*Resource) (bool, error) {
|
||||
return false, fmt.Errorf("filter error")
|
||||
})
|
||||
req.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleSetWalk(t *testing.T) {
|
||||
var (
|
||||
value = make(RuleSet, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// check walk with no errors
|
||||
{
|
||||
err := value.Walk(func(*Rule) error {
|
||||
return nil
|
||||
})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
// check walk with error
|
||||
req.Error(value.Walk(func(*Rule) error { return fmt.Errorf("walk error") }))
|
||||
}
|
||||
|
||||
func TestRuleSetFilter(t *testing.T) {
|
||||
var (
|
||||
value = make(RuleSet, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// filter nothing
|
||||
{
|
||||
set, err := value.Filter(func(*Rule) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
req.NoError(err)
|
||||
req.Equal(len(set), len(value))
|
||||
}
|
||||
|
||||
// filter one item
|
||||
{
|
||||
found := false
|
||||
set, err := value.Filter(func(*Rule) (bool, error) {
|
||||
if !found {
|
||||
found = true
|
||||
return found, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
req.NoError(err)
|
||||
req.Len(set, 1)
|
||||
}
|
||||
|
||||
// filter error
|
||||
{
|
||||
_, err := value.Filter(func(*Rule) (bool, error) {
|
||||
return false, fmt.Errorf("filter error")
|
||||
})
|
||||
req.Error(err)
|
||||
}
|
||||
}
|
||||
6
pkg/rbac/types.yaml
Normal file
6
pkg/rbac/types.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
package: rbac
|
||||
types:
|
||||
Rule:
|
||||
noIdField: true
|
||||
Resource:
|
||||
noIdField: true
|
||||
Reference in New Issue
Block a user