diff --git a/CHANGELOG.md b/CHANGELOG.md index e984d46e..e9e0fed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Semantic Versioning Changelog +# [0.20.0](https://github.com/casbin/pycasbin/compare/v0.19.2...v0.20.0) (2021-04-06) + + +### Features + +* added distributed enforcer file along with respective unit tests ([f167ebf](https://github.com/casbin/pycasbin/commit/f167ebf40f5d170745a3f48692d2185e205f3449)) + ## [0.19.2](https://github.com/casbin/pycasbin/compare/v0.19.1...v0.19.2) (2021-04-01) diff --git a/casbin/__init__.py b/casbin/__init__.py index df423716..9991b3ab 100644 --- a/casbin/__init__.py +++ b/casbin/__init__.py @@ -1,5 +1,6 @@ from .enforcer import * from .synced_enforcer import SyncedEnforcer +from .distributed_enforcer import DistributedEnforcer from . import util from .persist import * from .effect import * diff --git a/casbin/distributed_enforcer.py b/casbin/distributed_enforcer.py new file mode 100644 index 00000000..afb67f9a --- /dev/null +++ b/casbin/distributed_enforcer.py @@ -0,0 +1,132 @@ +from casbin import SyncedEnforcer +import logging + +from casbin.persist import batch_adapter +from casbin.model.policy_op import PolicyOp +from casbin.persist.adapters import update_adapter + + +class DistributedEnforcer(SyncedEnforcer): + """DistributedEnforcer wraps SyncedEnforcer for dispatcher.""" + + def __init__(self, model=None, adapter=None): + self.logger = logging.getLogger() + SyncedEnforcer.__init__(self, model, adapter) + + def add_policy_self(self, should_persist, sec, ptype, rules): + """ + AddPolicySelf provides a method for dispatcher to add authorization rules to the current policy. + The function returns the rules affected and error. + """ + + no_exists_policy = [] + for rule in rules: + if not self.get_model().has_policy(sec, ptype, rule): + no_exists_policy.append(rule) + + if should_persist: + try: + if isinstance(self.adapter, batch_adapter): + self.adapter.add_policies(sec, ptype, rules) + except Exception as e: + self.logger.log("An error occurred: " + e) + + self.get_model().add_policies(sec, ptype, no_exists_policy) + + if sec == "g": + try: + self.build_incremental_role_links(PolicyOp.Policy_add, ptype, no_exists_policy) + except Exception as e: + self.logger.log("An exception occurred: " + e) + return no_exists_policy + + return no_exists_policy + + def remove_policy_self(self, should_persist, sec, ptype, rules): + """ + remove_policy_self provides a method for dispatcher to remove policies from current policy. + The function returns the rules affected and error. + """ + if(should_persist): + try: + if(isinstance(self.adapter, batch_adapter)): + self.adapter.remove_policy(sec, ptype, rules) + except Exception as e: + self.logger.log("An exception occurred: " + e) + + effected = self.get_model().remove_policies_with_effected(sec, ptype, rules) + + if sec == "g": + try: + self.build_incremental_role_links(PolicyOp.Policy_remove, ptype, rules) + except Exception as e: + self.logger.log("An exception occurred: " + e) + return effected + + return effected + + def remove_filtered_policy_self(self, should_persist, sec, ptype, field_index, *field_values): + """ + remove_filtered_policy_self provides a method for dispatcher to remove an authorization + rule from the current policy,field filters can be specified. + The function returns the rules affected and error. + """ + if should_persist: + try: + self.adapter.remove_filtered_policy(sec, ptype, field_index, field_values) + except Exception as e: + self.logger.log("An exception occurred: " + e) + + effects = self.get_model().remove_filtered_policy_returns_effects(sec, ptype, field_index, field_values) + + if sec == "g": + try: + self.build_incremental_role_links(PolicyOp.Policy_remove, ptype, effects) + except Exception as e: + self.logger.log("An exception occurred: " + e) + return effects + + return effects + + def clear_policy_self(self, should_persist): + """ + clear_policy_self provides a method for dispatcher to clear all rules from the current policy. + """ + if should_persist: + try: + self.adapter.save_policy(None) + except Exception as e: + self.logger.log("An exception occurred: " + e) + + self.get_model().clear_policy() + + def update_policy_self(self, should_persist, sec, ptype, old_rule, new_rule): + """ + update_policy_self provides a method for dispatcher to update an authorization rule from the current policy. + """ + if should_persist: + try: + if isinstance(self.adapter, update_adapter): + self.adapter.update_policy(sec, ptype, old_rule, new_rule) + except Exception as e: + self.logger.log("An exception occurred: " + e) + return False + + rule_updated = self.get_model().update_policy(sec, ptype, old_rule, new_rule) + + if not rule_updated: + return False + + if sec == "g": + try: + self.build_incremental_role_links(PolicyOp.Policy_remove, ptype, [old_rule]) + except Exception as e: + return False + + try: + self.build_incremental_role_links(PolicyOp.Policy_add, ptype, [new_rule]) + except Exception as e: + return False + + + return True \ No newline at end of file diff --git a/casbin/internal_enforcer.py b/casbin/internal_enforcer.py index 871a2fdd..60138c10 100644 --- a/casbin/internal_enforcer.py +++ b/casbin/internal_enforcer.py @@ -1,4 +1,5 @@ from casbin.core_enforcer import CoreEnforcer +from casbin.model.policy_op import PolicyOp class InternalEnforcer(CoreEnforcer): """ diff --git a/casbin/model/assertion.py b/casbin/model/assertion.py index 476657a0..a5a803ea 100644 --- a/casbin/model/assertion.py +++ b/casbin/model/assertion.py @@ -1,5 +1,5 @@ import logging - +from casbin.model.policy_op import PolicyOp class Assertion: def __init__(self): @@ -24,3 +24,24 @@ def build_role_links(self, rm): self.logger.info("Role links for: {}".format(self.key)) self.rm.print_roles() + + def build_incremental_role_links(self, rm, op, rules): + self.rm = rm + count = 0 + for i in range(len(self.value)): + if self.value[i] == "_": + count += 1 + + for rule in rules: + if count < 2: + raise TypeError("the number of \"_\" in role definition should be at least 2") + if len(rule) < count: + raise TypeError("grouping policy elements do not meet role definition") + if(len(rule) > count): + rule = rule[0, count] + if op == PolicyOp.Policy_add: + rm.add_link(rule[0], rule[1], rule[2: len(rule)]) + elif op == PolicyOp.Policy_remove: + rm.delete_link(rule[0], rule[1], rule[2: len(rule)]) + else: + raise TypeError("Invalid operation: " + str(op)) \ No newline at end of file diff --git a/casbin/model/model.py b/casbin/model/model.py index f3e197d6..cd998842 100644 --- a/casbin/model/model.py +++ b/casbin/model/model.py @@ -2,7 +2,6 @@ from casbin import util, config from .policy import Policy - class Model(Policy): section_name_map = { diff --git a/casbin/model/policy.py b/casbin/model/policy.py index ed17a59b..d56301f1 100644 --- a/casbin/model/policy.py +++ b/casbin/model/policy.py @@ -15,6 +15,10 @@ def build_role_links(self, rm_map): rm = rm_map[ptype] ast.build_role_links(rm) + def build_incremental_role_links(self, rm, op, sec, ptype, rules): + if sec == "g": + self.model.get(sec).get(ptype).build_incremental_role_links(rm, op, rules) + def print_policy(self): """Log using info""" @@ -116,6 +120,40 @@ def remove_policies(self, sec, ptype, rules): return True + def remove_policies_with_effected(self, sec, ptype, rules): + effected = [] + for rule in rules: + if self.has_policy(sec, ptype, rule): + effected.append(rule) + self.remove_policy(sec, ptype, rule) + + return effected + + def remove_filtered_policy_returns_effects(self, sec, ptype, field_index, *field_values): + """ + remove_filtered_policy_returns_effects removes policy rules based on field filters from the model. + """ + tmp = [] + effects = [] + + if(len(field_values) == 0): + return [] + if sec not in self.model.keys(): + return [] + if ptype not in self.model[sec]: + return [] + + for rule in self.model[sec][ptype].policy: + if all(value == "" or rule[field_index + i] == value for i, value in enumerate(field_values[0])): + effects.append(rule) + else: + tmp.append(rule) + + self.model[sec][ptype].policy = tmp + + return effects + + def remove_filtered_policy(self, sec, ptype, field_index, *field_values): """removes policy rules based on field filters from the model.""" tmp = [] diff --git a/casbin/model/policy_op.py b/casbin/model/policy_op.py new file mode 100644 index 00000000..de4b146e --- /dev/null +++ b/casbin/model/policy_op.py @@ -0,0 +1,5 @@ +import enum + +class PolicyOp(enum.Enum): + Policy_add = 1 + Policy_remove = 2 \ No newline at end of file diff --git a/casbin/persist/adapters/update_adapter.py b/casbin/persist/adapters/update_adapter.py new file mode 100644 index 00000000..11d4f3fe --- /dev/null +++ b/casbin/persist/adapters/update_adapter.py @@ -0,0 +1,9 @@ +class UpdateAdapter: + """ UpdateAdapter is the interface for Casbin adapters with add update policy function. """ + + def update_policy(self, sec, ptype, old_rule, new_policy): + """ + update_policy updates a policy rule from storage. + This is part of the Auto-Save feature. + """ + pass \ No newline at end of file diff --git a/casbin/rbac/default_role_manager/role_manager.py b/casbin/rbac/default_role_manager/role_manager.py index a07c6130..f58defe3 100644 --- a/casbin/rbac/default_role_manager/role_manager.py +++ b/casbin/rbac/default_role_manager/role_manager.py @@ -47,8 +47,9 @@ def clear(self): def add_link(self, name1, name2, *domain): if len(domain) == 1: - name1 = domain[0] + "::" + name1 - name2 = domain[0] + "::" + name2 + if len(domain[0]) > 1: + name1 = domain[0] + "::" + name1 + name2 = domain[0] + "::" + name2 elif len(domain) > 1: raise RuntimeError("error: domain should be 1 parameter") @@ -69,8 +70,9 @@ def add_link(self, name1, name2, *domain): def delete_link(self, name1, name2, *domain): if len(domain) == 1: - name1 = domain[0] + "::" + name1 - name2 = domain[0] + "::" + name2 + if len(domain[0]) > 1: + name1 = domain[0] + "::" + name1 + name2 = domain[0] + "::" + name2 elif len(domain) > 1: raise RuntimeError("error: domain should be 1 parameter") diff --git a/casbin/synced_enforcer.py b/casbin/synced_enforcer.py index 8f43ebf4..4313af91 100644 --- a/casbin/synced_enforcer.py +++ b/casbin/synced_enforcer.py @@ -566,4 +566,7 @@ def remove_grouping_policies(self,rules): def remove_named_grouping_policies(self,ptype,rules): """ removes role inheritance rules from the current named policy.""" with self._wl: - return self._e.remove_named_grouping_policies(ptype,rules) \ No newline at end of file + return self._e.remove_named_grouping_policies(ptype,rules) + + def build_incremental_role_links(self, op, ptype, rules): + self.get_model().build_incremental_role_links(self.get_role_manager(), op, "g", ptype, rules) \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 5fd04003..df041459 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [metadata] -version = 0.19.2 +version = 0.20.0 diff --git a/tests/test_distributed_api.py b/tests/test_distributed_api.py new file mode 100644 index 00000000..36e2dbeb --- /dev/null +++ b/tests/test_distributed_api.py @@ -0,0 +1,82 @@ +import casbin +from tests.test_enforcer import get_examples, TestCaseBase + + +class TestDistributedApi(TestCaseBase): + + def get_enforcer(self, model=None, adapter=None): + return casbin.DistributedEnforcer( + model, + adapter, + ) + + def test(self): + e = self.get_enforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv") + ) + + e.add_policy_self(False, "p", "p", [ + ["alice", "data1", "read"], + ["bob", "data2", "write"], + ["data2_admin", "data2", "read"], + ["data2_admin", "data2", "write"] + ]) + e.add_policy_self(False, "g", "g", [["alice", "data2_admin"]]) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "write")) + self.assertTrue(e.enforce("alice", "data2", "read")) + self.assertTrue(e.enforce("alice", "data2", "write")) + + e.update_policy_self(False, "p", "p", ["alice", "data1", "read"],["alice", "data1", "write"]) + e.update_policy_self(False, "g", "g", ["alice", "data2_admin"], ["tom", "alice"]) + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertTrue(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "write")) + self.assertFalse(e.enforce("tom", "data1", "read")) + self.assertTrue(e.enforce("tom", "data1", "write")) + + e.remove_policy_self(False, "p", "p", [ + ["alice", "data1", "write"] + ]) + e.remove_policy_self(False, "g", "g", [ + ["alice", "data2_admin"] + ]) + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + + e.remove_filtered_policy_self(False, "p", "p", 0, "bob", "data2", "write") + e.remove_filtered_policy_self(False, "g", "g", 0, "tom", "data2_admin") + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertFalse(e.enforce("bob", "data2", "write")) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "write")) + self.assertFalse(e.enforce("tom", "data1", "read")) + self.assertFalse(e.enforce("tom", "data1", "write")) + + e.clear_policy_self(False) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertFalse(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("data2_admin", "data2", "read")) + self.assertFalse(e.enforce("data2_admin", "data2", "write"))