Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
frolad committed May 9, 2022
1 parent 4a44164 commit b720c62
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func policiesSetter(
switch access {

case AccessCanView:
if _, ok := contentPublic[Content]; ok {
if _, ok := contentPublic[Content]; ok {
return true
} else if user, ok := contentOwners[Content]; ok {
return owner == User;
Expand Down
154 changes: 154 additions & 0 deletions cbac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package gocbac

import (
"errors"
"fmt"

"golang.org/x/exp/maps"
)

// Possible errors
// ErrNoContent in case there is no such content in the policy
// ErrNoAccess in case there is no such access in the policy(s)
var (
ErrNoContent = errors.New("no such content")
ErrNoAccess = errors.New("no such access")
)

// PoliciesSetter function which populates policies for the list of content, error will be passed to the executor (e.g. GetPolicies)
// AccessSetter function which populates access for the peace of content
type (
PoliciesSetter[A, C comparable, O any] func(ContentList []C, On O, requestedAccesses []A) (AccessSetter[A, C], error)
AccessSetter[A, C comparable] func(content C, access A) bool
)

// GetPolicies get the list of policies for the list of content on On instance (optionally for the list of accesses)
// GetPolicy get the policy for the content on On instance (optionally for the list of accesses)
// GetAccess get the access for the content on On instance for the specific access
type CBAC[A, C comparable, O any] interface {
GetPolicies(ContentList []C, On O, Accesses ...A) (Policies[A, C], error)
GetPolicy(Content C, On O, Accesses ...A) (Policy[A], error)
GetAccess(Content C, On O, Access A) (bool, error)
}

// cbac stores policies setter as well as list of accesses
type cbac[A, C comparable, O any] struct {
accesses map[A]bool
setter PoliciesSetter[A, C, O]
}

// Init CBAC where:
// A - type of access
// C - type of content
// O - type of instance of which access should be checked
// Setter - policies setter function
// Accesses - list of needed accesses
func InitCBAC[A, C comparable, O any](Setter PoliciesSetter[A, C, O], Accesses ...A) CBAC[A, C, O] {
return &cbac[A, C, O]{
accesses: SliceToBoolMap(Accesses),
setter: Setter,
}
}

// GetPolicies get the list of policies for the list of content on On instance (optionally for the list of accesses)
func (c *cbac[A, C, O]) GetPolicies(ContentList []C, On O, requestedAccesses ...A) (Policies[A, C], error) {
possibleAccesses, err := c.cleanUpReqeustAccesses(requestedAccesses)
if err != nil {
return Policies[A, C]{}, err
}

// get initial policies
policies, err := c.preparePolicies(ContentList, possibleAccesses)
if err != nil {
return policies, err
}

acessSetter, err := c.setter(ContentList, On, possibleAccesses)
if err != nil {
return policies, err
}

for content, policy := range policies {
for access := range policy {
policies[content][access] = acessSetter(content, access)
}
}

// remove unrequested acceses in case getter have set them
return c.cleanUpPolicies(policies, possibleAccesses), nil
}

// GetPolicy get the policy for the content on On instance (optionally for the list of accesses)
func (c *cbac[A, C, O]) GetPolicy(Content C, On O, Accesses ...A) (Policy[A], error) {
policies, err := c.GetPolicies([]C{Content}, On, Accesses...)
if err != nil {
return Policy[A]{}, err
}

if policy, ok := policies[Content]; ok {
return policy, nil
}

return Policy[A]{}, ErrNoContent
}

// GetAccess get the access for the content on On instance for the specific access
func (c *cbac[A, C, O]) GetAccess(Content C, On O, Access A) (bool, error) {
policies, err := c.GetPolicies([]C{Content}, On, []A{Access}...)
if err != nil {
return false, err
}

if policy, ok := policies[Content]; ok {
return policy[Access], nil
}

return false, fmt.Errorf("%w: "+fmt.Sprintf("%v", Access), ErrNoAccess)
}

// clean up requested accesses
// in case requested access is not in the list of original accesses - return error
func (c *cbac[A, C, O]) cleanUpReqeustAccesses(requestedAccesses []A) ([]A, error) {
keys := []A{}

if len(requestedAccesses) > 0 {
for _, access := range requestedAccesses {
if _, ok := c.accesses[access]; ok {
keys = append(keys, access)
} else {
return keys, fmt.Errorf("%w: "+fmt.Sprintf("%v", access), ErrNoAccess)
}
}
} else {
keys = maps.Keys(c.accesses)
}

return keys, nil
}

// created empty list of policies for the list of contents
func (c *cbac[A, C, O]) preparePolicies(ContentList []C, requestedAccesses []A) (Policies[A, C], error) {
res := Policies[A, C]{}
for _, id := range ContentList {
res[id] = MapFill(Policy[A]{}, requestedAccesses, false)
}

return res, nil
}

// clean up policies - keep only accesses which were provided in the InitCBAC and ignore the rest
func (c *cbac[A, C, O]) cleanUpPolicies(policies Policies[A, C], requestedAccesses []A) Policies[A, C] {
for key, policy := range policies {
cleanPolicy := MapFill(Policy[A]{}, requestedAccesses, false)

for _, access := range requestedAccesses {
if val, ok := policy[access]; ok {
cleanPolicy[access] = val
}
}

policies[key] = cleanPolicy
}

return policies
}
81 changes: 81 additions & 0 deletions cbac_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package gocbac

import (
"errors"
"testing"
)

type Access string

type Content struct {
ID uint64
}

type User struct {
Email string
}

const (
AccessCanView Access = "can_view"
AccessCanEdit Access = "can_edit"
AccessCanDelete Access = "can_delete"
)

func policiesSetter(
ContentList []Content,
User User,
RequestedAccesses []Access,
) (AccessSetter[Access, Content], error) {
if User.Email == "[email protected]" {
return nil, errors.New("Error in setter")
}

return func(Content Content, access Access) bool {
if Content.ID == 1 && User.Email == "[email protected]" {
return true
}

return false
}, nil
}

var cbacInstance = InitCBAC(
policiesSetter,
AccessCanView,
AccessCanEdit,
AccessCanDelete,
)

func TestCorrectAccess(t *testing.T) {
has, err := cbacInstance.GetAccess(Content{ID: 1}, User{Email: "[email protected]"}, AccessCanView)
if err != nil {
t.Error(err)
}

if !has {
t.Error("Access value is incorrect for [email protected]")
}

has, err = cbacInstance.GetAccess(Content{ID: 1}, User{Email: "[email protected]"}, AccessCanView)
if err != nil {
t.Error(err)
}

if has {
t.Error("Access value is incorrect for [email protected]")
}
}

func TestIncorrectAccess(t *testing.T) {
_, err := cbacInstance.GetAccess(Content{ID: 1}, User{Email: "[email protected]"}, "random-access")
if !errors.Is(err, ErrNoAccess) {
t.Error(err)
}
}

func TestSetterError(t *testing.T) {
_, err := cbacInstance.GetAccess(Content{ID: 1}, User{Email: "[email protected]"}, AccessCanView)
if err == nil || err.Error() != "Error in setter" {
t.Error("Incorrect setter error")
}
}
21 changes: 21 additions & 0 deletions generics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gocbac

// convert slice to the map where each element of a slice is the key and values is bool
func SliceToBoolMap[S ~[]K, K comparable](slice S) map[K]bool {
res := map[K]bool{}

for _, item := range slice {
res[item] = true
}

return res
}

// fill map with same values
func MapFill[M ~map[K]V, K comparable, V any](dict M, keys []K, val V) M {
for _, key := range keys {
dict[key] = val
}

return dict
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/frolad/gocbac

go 1.18

require golang.org/x/exp v0.0.0-20220428152302-39d4317da171
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
16 changes: 16 additions & 0 deletions policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gocbac

type Policies[A comparable, C comparable] map[C]Policy[A]

func (p Policies[A, C]) Set(setter func(content C, access A) bool) Policies[A, C] {
for content, policy := range p {
for access := range policy {
p[content][access] = setter(content, access)
}
}

return p
}

// policy is the simple map of accesses and their bool values
type Policy[A comparable] map[A]bool

0 comments on commit b720c62

Please sign in to comment.