Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,33 @@ Adapter | Type | Author | Description

For details of adapters, please refer to the documentation: https://github.com/casbin/casbin/wiki/Policy-persistence

## Policy enforcement at scale

Some adapters support filtered policy management. This means that the policy loaded by Casbin is a subset of the policy in storage based on a given filter. This allows for efficient policy enforcement in large, multi-tenant environments when parsing the entire policy becomes a performance bottleneck.

To use filtered policies with a supported adapter, simply call the `LoadFilteredPolicy` method. The valid format for the filter parameter depends on the adapter used. To prevent accidental data loss, the `SavePolicy` method is disabled when a filtered policy is loaded.

For example, the following code snippet uses the built-in filtered file adapter and the RBAC model with domains. In this case, the filter limits the policy to a single domain. Any policy lines for domains other than `"domain1"` are omitted from the loaded policy:

```go
import (
"github.com/casbin/casbin"
)

enforcer := casbin.NewEnforcer()

adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
enforcer.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)

filter := &fileadapter.Filter{
P: []string{"", "domain1"},
G: []string{"", "", "domain1"},
}
enforcer.LoadFilteredPolicy(filter)

// The loaded policy now only contains the entries pertaining to "domain1".
```

## Policy consistence between multiple nodes

We support to use distributed messaging systems like [etcd](https://github.com/coreos/etcd) to keep consistence between multiple Casbin enforcer instances. So our users can concurrently use multiple Casbin enforcers to handle large number of permission checking requests.
Expand Down
2 changes: 1 addition & 1 deletion effect/effector.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Effect int

// Values for policy effect.
const (
Allow Effect = iota
Allow Effect = iota
Indeterminate
Deny
)
Expand Down
35 changes: 35 additions & 0 deletions enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,43 @@ func (e *Enforcer) LoadPolicy() error {
return nil
}

// LoadFilteredPolicy reloads a filtered policy from file/database.
func (e *Enforcer) LoadFilteredPolicy(filter interface{}) error {
e.model.ClearPolicy()

// Attempt to cast the Adapter as a FilteredAdapter
a := reflect.ValueOf(e.adapter)
filteredAdapter, ok := a.Interface().(persist.FilteredAdapter)
if !ok {
return errors.New("filtered policies are not supported by this adapter")
}
err := filteredAdapter.LoadFilteredPolicy(e.model, filter)
if err != nil {
return err
}

e.model.PrintPolicy()
if e.autoBuildRoleLinks {
e.BuildRoleLinks()
}
return nil
}

// IsFiltered returns true if the loaded policy has been filtered.
func (e *Enforcer) IsFiltered() bool {
a := reflect.ValueOf(e.adapter)
filteredAdapter, ok := a.Interface().(persist.FilteredAdapter)
if !ok {
return false
}
return filteredAdapter.IsFiltered()
}

// SavePolicy saves the current policy (usually after changed with Casbin API) back to file/database.
func (e *Enforcer) SavePolicy() error {
if e.IsFiltered() {
return errors.New("cannot save a filtered policy")
}
err := e.adapter.SavePolicy(e.model)
if err == nil {
if e.watcher != nil {
Expand Down
96 changes: 95 additions & 1 deletion enforcer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,98 @@ func TestInitEmpty(t *testing.T) {
e.LoadPolicy()

testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true)
}
}

func TestLoadFilteredPolicy(t *testing.T) {
e := NewEnforcer()

adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)

// validate initial conditions
testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, true)
testHasPolicy(t, e, []string{"admin", "domain2", "data2", "read"}, true)

if err := e.LoadFilteredPolicy(&fileadapter.Filter{
P: []string{"", "domain1"},
G: []string{"", "", "domain1"},
}); err != nil {
t.Errorf("unexpected error in LoadFilteredPolicy: %v", err)
}
if !e.IsFiltered() {
t.Errorf("adapter did not set the filtered flag correctly")
}

// only policies for domain1 should be loaded
testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, true)
testHasPolicy(t, e, []string{"admin", "domain2", "data2", "read"}, false)

if err := e.SavePolicy(); err == nil {
t.Errorf("enforcer did not prevent saving filtered policy")
}
if err := e.GetAdapter().SavePolicy(e.GetModel()); err == nil {
t.Errorf("adapter did not prevent saving filtered policy")
}
}

func TestFilteredPolicyInvalidFilter(t *testing.T) {
e := NewEnforcer()

adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)

if err := e.LoadFilteredPolicy([]string{"", "domain1"}); err == nil {
t.Errorf("expected error in LoadFilteredPolicy, but got nil")
}
}

func TestFilteredPolicyEmptyFilter(t *testing.T) {
e := NewEnforcer()

adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)

if err := e.LoadFilteredPolicy(nil); err != nil {
t.Errorf("unexpected error in LoadFilteredPolicy: %v", err)
}
if e.IsFiltered() {
t.Errorf("adapter did not reset the filtered flag correctly")
}
if err := e.SavePolicy(); err != nil {
t.Errorf("unexpected error in SavePolicy: %v", err)
}
}

func TestUnsupportedFilteredPolicy(t *testing.T) {
e := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv")

err := e.LoadFilteredPolicy(&fileadapter.Filter{
P: []string{"", "domain1"},
G: []string{"", "", "domain1"},
})
if err == nil {
t.Errorf("encorcer should have reported incompatibility error")
}
}

func TestFilteredAdapterEmptyFilepath(t *testing.T) {
e := NewEnforcer()

adapter := fileadapter.NewFilteredAdapter("")
e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)

if err := e.LoadFilteredPolicy(nil); err == nil {
t.Errorf("expected error in LoadFilteredPolicy, but got nil")
}
}

func TestFilteredAdapterInvalidFilepath(t *testing.T) {
e := NewEnforcer()

adapter := fileadapter.NewFilteredAdapter("examples/does_not_exist_policy.csv")
e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)

if err := e.LoadFilteredPolicy(nil); err == nil {
t.Errorf("expected error in LoadFilteredPolicy, but got nil")
}
}
1 change: 0 additions & 1 deletion examples/rbac_with_domains_policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ p, admin, domain1, data1, read
p, admin, domain1, data1, write
p, admin, domain2, data2, read
p, admin, domain2, data2, write

g, alice, admin, domain1
g, bob, admin, domain2
29 changes: 29 additions & 0 deletions persist/adapter_filtered.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2017 The casbin Authors. All Rights Reserved.
//
// 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 persist

import (
"github.com/casbin/casbin/model"
)

// FilteredAdapter is the interface for Casbin adapters supporting filtered policies.
type FilteredAdapter interface {
Adapter

// LoadFilteredPolicy loads only policy rules that match the filter.
LoadFilteredPolicy(model model.Model, filter interface{}) error
// IsFiltered returns true if the loaded policy has been filtered.
IsFiltered() bool
}
15 changes: 4 additions & 11 deletions persist/file-adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"bufio"
"bytes"
"errors"
"io"
"os"
"strings"

Expand Down Expand Up @@ -85,18 +84,12 @@ func (a *Adapter) loadPolicyFile(model model.Model, handler func(string, model.M
}
defer f.Close()

buf := bufio.NewReader(f)
for {
line, err := buf.ReadString('\n')
line = strings.TrimSpace(line)
scanner := bufio.NewScanner(f)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the opportunity to integrate my streamlined handler into the base file adapter, as it will be simpler for others to use as a reference IMO.

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
handler(line, model)
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
return scanner.Err()
}

func (a *Adapter) savePolicyFile(text string) error {
Expand Down
136 changes: 136 additions & 0 deletions persist/file-adapter/adapter_filtered.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2017 The casbin Authors. All Rights Reserved.
//
// 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 fileadapter

import (
"bufio"
"errors"
"os"
"strings"

"github.com/casbin/casbin/model"
"github.com/casbin/casbin/persist"
)

// FilteredAdapter is the filtered file adapter for Casbin. It can load policy
// from file or save policy to file and supports loading of filtered policies.
type FilteredAdapter struct {
*Adapter
filtered bool
}

// Filter defines the filtering rules for a FilteredAdapter's policy. Empty values
// are ignored, but all others must match the filter.
type Filter struct {
P []string
G []string
}

// NewFilteredAdapter is the constructor for FilteredAdapter.
func NewFilteredAdapter(filePath string) *FilteredAdapter {
a := FilteredAdapter{}
a.Adapter = NewAdapter(filePath)
return &a
}

func (a *FilteredAdapter) LoadPolicy(model model.Model) error {
a.filtered = false
return a.Adapter.LoadPolicy(model)
}

// LoadPolicy loads all policy rules from the storage.
func (a *FilteredAdapter) LoadFilteredPolicy(model model.Model, filter interface{}) error {
if filter == nil {
return a.LoadPolicy(model)
}
if a.filePath == "" {
return errors.New("invalid file path, file path cannot be empty")
}

filterValue, ok := filter.(*Filter)
if !ok {
return errors.New("invalid filter type")
}
err := a.loadFilteredPolicyFile(model, filterValue, persist.LoadPolicyLine)
if err == nil {
a.filtered = true
}
return err
}

func (a *FilteredAdapter) loadFilteredPolicyFile(model model.Model, filter *Filter, handler func(string, model.Model)) error {
f, err := os.Open(a.filePath)
if err != nil {
return err
}
defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())

if filterLine(line, filter) {
continue
}

handler(line, model)
}
return scanner.Err()
}

// IsFiltered returns true if the loaded policy has been filtered.
func (a *FilteredAdapter) IsFiltered() bool {
return a.filtered
}

// SavePolicy saves all policy rules to the storage.
func (a *FilteredAdapter) SavePolicy(model model.Model) error {
if a.filtered == true {
return errors.New("cannot save a filtered policy")
}
return a.Adapter.SavePolicy(model)
}

func filterLine(line string, filter *Filter) bool {
if filter == nil {
return false
}
p := strings.Split(line, ",")
if len(p) == 0 {
return true
}
var filterSlice []string
switch strings.TrimSpace(p[0]) {
case "p":
filterSlice = filter.P
case "g":
filterSlice = filter.G
}
return filterWords(p, filterSlice)
}

func filterWords(line []string, filter []string) bool {
if len(line) < len(filter)+1 {
return true
}
var skipLine bool
for i, v := range filter {
if len(v) > 0 && strings.TrimSpace(v) != strings.TrimSpace(line[i+1]) {
skipLine = true
break
}
}
return skipLine
}