3
0

Permission rules refactored

This commit is contained in:
Denis Arh
2019-05-08 16:19:50 +02:00
parent 6e00826dc8
commit 087ba7c92d
11 changed files with 661 additions and 0 deletions

View File

@@ -53,6 +53,9 @@ function types {
./build/gen-type-set --types Value --output internal/settings/types.gen.go --with-primary-key=false --package settings
./build/gen-type-set --types Rule --output internal/permissions/rule.gen.go --with-primary-key=false --package permissions
./build/gen-type-set --types Resource --output internal/permissions/resource.gen.go --with-primary-key=false --package permissions
green "OK"
}

View File

@@ -0,0 +1,37 @@
package permissions
// General permission stuff, types, constants
type (
Operation string
Access int
)
const EveryoneRoleID = 1
func (a Access) String() string {
switch a {
case Allow:
return "allow"
case Deny:
return "deny"
default:
return "inherit"
}
}
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
}

View File

@@ -0,0 +1,49 @@
package permissions
import (
"context"
"github.com/titpetric/factory"
)
type (
// repository servs as a db storage layer for permission rules
repository struct {
dbh *factory.DB
// sql table reference
dbTable string
}
)
func Repository(db *factory.DB, table string) *repository {
return &repository{
dbTable: table,
dbh: db,
}
}
func (r *repository) db() *factory.DB {
return r.dbh
}
func (r repository) columns() []string {
return []string{
"rel_role",
"resource",
"operation",
"value",
}
}
func (r *repository) With(ctx context.Context) *repository {
return &repository{
dbTable: r.dbTable,
dbh: r.db().With(ctx),
}
}
func (r *repository) Load() (rr RuleSet, err error) {
// @todo load and return
return nil, nil
}

View File

@@ -0,0 +1,41 @@
package permissions
// Hello! This file is auto-generated.
type (
// ResourceSet slice of Resource
//
// This type is auto-generated.
ResourceSet []*Resource
)
// 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
}

View File

@@ -0,0 +1,77 @@
package permissions
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) PermissionResource() Resource {
return r
}
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)
}

View File

@@ -0,0 +1,63 @@
package permissions
import (
"testing"
"github.com/crusttech/crust/internal/test"
)
func TestResource(t *testing.T) {
var (
assert = test.Assert
sCases = []struct {
r Resource
s string
}{
{
Resource("a:b:c"),
"a:b:c"},
{
Resource("a:b:c").PermissionResource(),
"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 {
assert(t, sc.r.String() == sc.s, "Resource check failed (%s != %s)", sc.r, sc.s)
}
var r string
r = "a:"
assert(t, Resource(r).IsAppendable(), "Expecting resource %q to be appendable", r)
r = "a:1"
assert(t, Resource(r).IsAppendable(), "Expecting resource %q to be appendable", r)
r = "a:*"
assert(t, Resource(r).IsAppendable(), "Expecting resource %q to be appendable", r)
r = "a"
assert(t, Resource(r).IsValid(), "Expecting resource %q to be valid", r)
r = "a:"
assert(t, !Resource(r).IsValid(), "Expecting resource %q not to be valid", r)
r = "a:1"
assert(t, Resource(r).IsValid(), "Expecting resource %q to be valid", r)
r = "a:*"
assert(t, Resource(r).IsValid(), "Expecting resource %q to be valid", r)
r = "a:1"
assert(t, !Resource(r).HasWildcard(), "Expecting resource %q to not have wildcard", r)
r = "a:*"
assert(t, Resource(r).HasWildcard(), "Expecting resource %q to have wildcard", r)
}

View File

@@ -0,0 +1,41 @@
package permissions
// Hello! This file is auto-generated.
type (
// RuleSet slice of Rule
//
// This type is auto-generated.
RuleSet []*Rule
)
// 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
}

View File

@@ -0,0 +1,35 @@
package permissions
import (
"fmt"
)
type (
Rule struct {
RoleID uint64 `json:"roleID,string" db:"rel_role"`
Resource Resource `json:"resource" db:"resource"`
Operation Operation `json:"operation" db:"operation"`
Access Access `json:"value,string" db:"value"`
}
)
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 {
return r.RoleID == cmp.RoleID &&
r.Resource == cmp.Resource &&
r.Operation == cmp.Operation
}

View File

@@ -0,0 +1,88 @@
package permissions
// Check verifies if role has access to perform an operation on a resource
//
// Overall flow:
// - no roles, no access
// - invalid resource, no access
// - can this specific role perform an operation on this specific resource
// - can this specific role perform an operation on any resource of the type (wildcard)
// - can anyone perform an operation on this specific resource
// - can anyone perform an operation on any resource of the type (wildcard)
func (set RuleSet) Check(res Resource, op Operation, roles ...uint64) (v Access) {
if len(roles) == 0 {
return Deny
}
if !res.IsValid() {
return Deny
}
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 ito a wildcarded-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
// 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
}

View File

@@ -0,0 +1,164 @@
package permissions
import (
"testing"
"github.com/crusttech/crust/internal/test"
)
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 (
assert = test.Assert
rr = RuleSet{
&Rule{role1, resThing42, opRead, Allow},
&Rule{role1, resThing13, opWrite, Deny},
&Rule{role2, resThing13, opWrite, Allow},
}
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...)
assert(t, v == sc.expected, "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 (
assert = test.Assert
sCases = []struct {
rr RuleSet
roles []uint64
res Resource
op Operation
expected Access
}{
{
RuleSet{
&Rule{role1, resService1, opAccess, Allow},
},
[]uint64{role1},
resService1,
opAccess,
Allow,
},
{
RuleSet{
&Rule{role1, resThingWc, opAccess, Allow},
},
[]uint64{role1},
resThing42,
opAccess,
Allow,
},
{ // deny wc and explictly allow 42
RuleSet{
&Rule{role1, resThingWc, opAccess, Deny},
&Rule{role1, resThing42, opAccess, Allow},
},
[]uint64{role1},
resThing42,
opAccess,
Allow,
},
{ // deny wc and explictly allow 42
RuleSet{
&Rule{role1, resThingWc, opAccess, Deny},
&Rule{role1, resThing42, opAccess, Allow},
},
[]uint64{role1},
resThing13,
opAccess,
Deny,
},
}
)
for c, sc := range sCases {
v := sc.rr.checkResource(sc.res, sc.op, sc.roles...)
assert(t, v == sc.expected, "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
&Rule{role1, resService1, opAccess, Allow},
&Rule{role2, resService1, opAccess, Deny},
// 2nd level
&Rule{EveryoneRoleID, resService2, opAccess, Deny},
&Rule{role1, resService2, opAccess, Allow},
// 3rd level
&Rule{EveryoneRoleID, resThingWc, opAccess, Deny},
&Rule{role1, resThing42, opAccess, Allow},
}
assert = test.Assert
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},
}
)
for c, sc := range sCases {
v := rr.Check(sc.res, sc.op, sc.roles...)
assert(t, v == sc.expected, "Check test #%d failed, expected %s, got %s", c, sc.expected, v)
}
}

View File

@@ -0,0 +1,63 @@
package permissions
import (
"context"
"sync"
)
type (
service struct {
l sync.Locker
rules RuleSet
repository *repository
}
)
// Service initializes service{} struct
//
// service{} struct preloads, checks, grants and flushes privileges to and from repository
// It acts as a caching layer
func Service(repository *repository) *service {
return &service{
repository: repository,
}
}
func (svc *service) Preload(ctx context.Context) (err error) {
svc.l.Lock()
defer svc.l.Unlock()
svc.rules, err = svc.repository.With(ctx).Load()
if err != nil {
return
}
return nil
}
// 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, rules ...*Rule) error {
svc.l.Lock()
defer svc.l.Unlock()
// @todo update svc.rules
return nil
}
func (svc service) watcher() {
// @todo will listen to chan and load new stuff every time it gets a ping
}