diff --git a/README.md b/README.md index bac0c9e..e7d935f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ PyMongo Adapter for PyCasbin ==== -[![Build Status](https://www.travis-ci.org/officialpycasbin/pymongo-adapter.svg?branch=master)](https://www.travis-ci.org/officialpycasbin/pymongo-adapter) +[![build Status](https://github.com/officialpycasbin/pymongo-adapter/actions/workflows/main.yml/badge.svg)](https://github.com/officialpycasbin/pymongo-adapter/actions/workflows/main.yml) [![Coverage Status](https://coveralls.io/repos/github/officialpycasbin/pymongo-adapter/badge.svg)](https://coveralls.io/github/officialpycasbin/pymongo-adapter) [![Version](https://img.shields.io/pypi/v/casbin_pymongo_adapter.svg)](https://pypi.org/project/casbin_pymongo_adapter/) [![PyPI - Wheel](https://img.shields.io/pypi/wheel/casbin_pymongo_adapter.svg)](https://pypi.org/project/casbin_pymongo_adapter/) [![Pyversions](https://img.shields.io/pypi/pyversions/casbin_pymongo_adapter.svg)](https://pypi.org/project/casbin_pymongo_adapter/) -[![Download](https://img.shields.io/pypi/dm/casbin_pymongo_adapter.svg)](https://pypi.org/project/casbin_pymongo_adapter/) +[![Download](https://static.pepy.tech/badge/casbin_pymongo_adapter)](https://pypi.org/project/casbin_pymongo_adapter/) [![License](https://img.shields.io/pypi/l/casbin_pymongo_adapter.svg)](https://pypi.org/project/casbin_pymongo_adapter/) PyMongo Adapter is the [PyMongo](https://pypi.org/project/pymongo/) adapter for [PyCasbin](https://github.com/casbin/pycasbin). With this library, Casbin can load policy from MongoDB or save policy to it. +This adapter supports both synchronous and asynchronous PyMongo APIs. + ## Installation ``` @@ -37,6 +39,38 @@ if e.enforce(sub, obj, act): else: # deny the request, show an error pass + +# define filter conditions +from casbin_pymongo_adapter import Filter + +filter = Filter() +filter.ptype = ["p"] +filter.v0 = ["alice"] + +# support MongoDB native query +filter.raw_query = { + "ptype": "p", + "v0": { + "$in": ["alice"] + } +} + +# In this case, load only policies with sub value alice +e.load_filtered_policy(filter) +``` + +## Async Example + +```python +from casbin_pymongo_adapter.asynchronous import Adapter +import casbin + +adapter = Adapter('mongodb://localhost:27017/', "dbname") +e = casbin.AsyncEnforcer('path/to/model.conf', adapter) + +# Note: AsyncEnforcer does not automatically load policies. +# You need to call load_policy() manually. +await e.load_policy() ``` diff --git a/casbin_pymongo_adapter/__init__.py b/casbin_pymongo_adapter/__init__.py index a7dde9b..095a8bb 100644 --- a/casbin_pymongo_adapter/__init__.py +++ b/casbin_pymongo_adapter/__init__.py @@ -1 +1,9 @@ from .adapter import Adapter +from ._filter import Filter +from ._rule import CasbinRule + +__all__ = [ + "Adapter", + "Filter", + "CasbinRule", +] diff --git a/casbin_pymongo_adapter/_filter.py b/casbin_pymongo_adapter/_filter.py new file mode 100644 index 0000000..5d1075f --- /dev/null +++ b/casbin_pymongo_adapter/_filter.py @@ -0,0 +1,16 @@ +class Filter: + """ + Filter rule model + """ + + ptype = [] + v0 = [] + v1 = [] + v2 = [] + v3 = [] + v4 = [] + v5 = [] + + # `raw_query` expected dict. + # if set `raw_query`, all other filters are ignored + raw_query = None diff --git a/casbin_pymongo_adapter/adapter.py b/casbin_pymongo_adapter/adapter.py index 45e2ebc..9472b9d 100644 --- a/casbin_pymongo_adapter/adapter.py +++ b/casbin_pymongo_adapter/adapter.py @@ -7,7 +7,13 @@ class Adapter(persist.Adapter): """the interface for Casbin adapters.""" - def __init__(self, uri, dbname, collection="casbin_rule"): + def __init__( + self, + uri, + dbname, + collection="casbin_rule", + filtered=False, + ): """Create an adapter for Mongodb Args: @@ -15,10 +21,15 @@ def __init__(self, uri, dbname, collection="casbin_rule"): See https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient. dbname (str): Database to store policy. collection (str, optional): Collection of the choosen database. Defaults to "casbin_rule". + filtered (bool, optional): Whether to use filtered query. Defaults to False. """ client = MongoClient(uri) db = client[dbname] self._collection = db[collection] + self._filtered = filtered + + def is_filtered(self): + return self._filtered def load_policy(self, model): """Implementing add Interface for casbin. Load all policy rules from mongodb @@ -36,6 +47,32 @@ def load_policy(self, model): persist.load_policy_line(str(rule), model) + def load_filtered_policy(self, model, filter): + """Load filtered policy rules from mongodb + + Args: + model (CasbinRule): CasbinRule object + filter (Filter): Filter rule object + """ + query = {} + if getattr(filter, "raw_query", None) is None: + for attr in ("ptype", "v0", "v1", "v2", "v3", "v4", "v5"): + if len(getattr(filter, attr)) > 0: + value = getattr(filter, attr) + query[attr] = {"$in": value} + else: + query = getattr(filter, "raw_query") + + for line in self._collection.find(query): + if "ptype" not in line: + continue + rule = CasbinRule(line["ptype"]) + for key, value in line.items(): + setattr(rule, key, value) + + persist.load_policy_line(str(rule), model) + self._filtered = True + def _save_policy_line(self, ptype, rule): line = CasbinRule(ptype=ptype) for index, value in enumerate(rule): diff --git a/casbin_pymongo_adapter/asynchronous/__init__.py b/casbin_pymongo_adapter/asynchronous/__init__.py index e69de29..2012980 100644 --- a/casbin_pymongo_adapter/asynchronous/__init__.py +++ b/casbin_pymongo_adapter/asynchronous/__init__.py @@ -0,0 +1,5 @@ +from .adapter import Adapter + +__all__ = [ + "Adapter", +] diff --git a/casbin_pymongo_adapter/asynchronous/adapter.py b/casbin_pymongo_adapter/asynchronous/adapter.py index b223a22..70a4aff 100644 --- a/casbin_pymongo_adapter/asynchronous/adapter.py +++ b/casbin_pymongo_adapter/asynchronous/adapter.py @@ -8,7 +8,13 @@ class Adapter(AsyncAdapter): """the interface for Casbin adapters.""" - def __init__(self, uri, dbname, collection="casbin_rule"): + def __init__( + self, + uri, + dbname, + collection="casbin_rule", + filtered=False, + ): """Create an adapter for Mongodb Args: @@ -16,10 +22,15 @@ def __init__(self, uri, dbname, collection="casbin_rule"): See https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient. dbname (str): Database to store policy. collection (str, optional): Collection of the choosen database. Defaults to "casbin_rule". + filtered (bool, optional): Whether to use filtered query. Defaults to False. """ client = AsyncMongoClient(uri) db = client[dbname] self._collection = db[collection] + self._filtered = filtered + + def is_filtered(self): + return self._filtered async def load_policy(self, model): """Implementing add Interface for casbin. Load all policy rules from mongodb @@ -37,6 +48,32 @@ async def load_policy(self, model): persist.load_policy_line(str(rule), model) + async def load_filtered_policy(self, model, filter): + """Load filtered policy rules from mongodb + + Args: + model (CasbinRule): CasbinRule object + filter (Filter): Filter rule object + """ + query = {} + if getattr(filter, "raw_query", None) is None: + for attr in ("ptype", "v0", "v1", "v2", "v3", "v4", "v5"): + if len(getattr(filter, attr)) > 0: + value = getattr(filter, attr) + query[attr] = {"$in": value} + else: + query = getattr(filter, "raw_query") + + async for line in self._collection.find(query): + if "ptype" not in line: + continue + rule = CasbinRule(line["ptype"]) + for key, value in line.items(): + setattr(rule, key, value) + + persist.load_policy_line(str(rule), model) + self._filtered = True + async def _save_policy_line(self, ptype, rule): line = CasbinRule(ptype=ptype) for index, value in enumerate(rule): diff --git a/tests/asynchronous/__init__.py b/tests/asynchronous/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/asynchronous/test_adapter.py b/tests/asynchronous/test_adapter.py index c29b82b..5a66104 100644 --- a/tests/asynchronous/test_adapter.py +++ b/tests/asynchronous/test_adapter.py @@ -1,5 +1,5 @@ -from casbin_pymongo_adapter.asynchronous.adapter import Adapter -from casbin_pymongo_adapter._rule import CasbinRule +from casbin_pymongo_adapter.asynchronous import Adapter +from casbin_pymongo_adapter import Filter from pymongo import AsyncMongoClient from unittest import IsolatedAsyncioTestCase import casbin @@ -196,18 +196,145 @@ async def test_remove_filtered_policy(self): self.assertFalse(e.enforce("alice", "data2", "read")) self.assertFalse(e.enforce("alice", "data2", "write")) - def test_str(self): + async def test_filtered_policy(self): """ - test __str__ function + test filtered_policy """ - rule = CasbinRule(ptype="p", v0="alice", v1="data1", v2="read") - self.assertEqual(rule.__str__(), "p, alice, data1, read") + e = await get_enforcer() + filter = Filter() + + filter.ptype = ["p"] + await e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + + filter.ptype = [] + filter.v0 = ["alice"] + await e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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")) + + filter.v0 = ["bob"] + await e.load_filtered_policy(filter) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("data2_admin", "data2", "read")) + self.assertFalse(e.enforce("data2_admin", "data2", "write")) + + filter.v0 = ["data2_admin"] + await e.load_filtered_policy(filter) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertFalse(e.enforce("bob", "data2", "write")) + + filter.v0 = ["alice", "bob"] + await e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("data2_admin", "data2", "read")) + self.assertFalse(e.enforce("data2_admin", "data2", "write")) + + filter.v0 = [] + filter.v1 = ["data1"] + await e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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")) + + filter.v1 = ["data2"] + await e.load_filtered_policy(filter) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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")) + + filter.v1 = [] + filter.v2 = ["read"] + await e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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.assertFalse(e.enforce("data2_admin", "data2", "write")) + + filter.v2 = ["write"] + await e.load_filtered_policy(filter) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "write")) - def test_dict(self): + async def test_filtered_policy_with_raw_query(self): """ - test __str__ function + test filtered_policy """ - rule = CasbinRule(ptype="p", v0="alice", v1="data1", v2="read") - self.assertEqual( - rule.dict(), {"ptype": "p", "v0": "alice", "v1": "data1", "v2": "read"} - ) + e = await get_enforcer() + filter = Filter() + filter.raw_query = {"ptype": "p", "v0": {"$in": ["alice", "bob"]}} + + await e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 70afd06..0da6f8c 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -1,5 +1,5 @@ -from casbin_pymongo_adapter.adapter import Adapter from casbin_pymongo_adapter._rule import CasbinRule +from casbin_pymongo_adapter import Filter, Adapter from pymongo import MongoClient from unittest import TestCase import casbin @@ -200,6 +200,149 @@ def test_remove_filtered_policy(self): self.assertFalse(e.enforce("alice", "data2", "read")) self.assertFalse(e.enforce("alice", "data2", "write")) + async def test_filtered_policy(self): + """ + test filtered_policy + """ + e = get_enforcer() + filter = Filter() + + filter.ptype = ["p"] + e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + + filter.ptype = [] + filter.v0 = ["alice"] + e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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")) + + filter.v0 = ["bob"] + e.load_filtered_policy(filter) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("data2_admin", "data2", "read")) + self.assertFalse(e.enforce("data2_admin", "data2", "write")) + + filter.v0 = ["data2_admin"] + e.load_filtered_policy(filter) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "read")) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertFalse(e.enforce("bob", "data2", "write")) + + filter.v0 = ["alice", "bob"] + e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("data2_admin", "data2", "read")) + self.assertFalse(e.enforce("data2_admin", "data2", "write")) + + filter.v0 = [] + filter.v1 = ["data1"] + e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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")) + + filter.v1 = ["data2"] + e.load_filtered_policy(filter) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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")) + + filter.v1 = [] + filter.v2 = ["read"] + e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "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.assertFalse(e.enforce("data2_admin", "data2", "write")) + + filter.v2 = ["write"] + e.load_filtered_policy(filter) + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertFalse(e.enforce("data2_admin", "data2", "read")) + self.assertTrue(e.enforce("data2_admin", "data2", "write")) + + async def test_filtered_policy_with_raw_query(self): + """ + test filtered_policy + """ + e = get_enforcer() + filter = Filter() + filter.raw_query = {"ptype": "p", "v0": {"$in": ["alice", "bob"]}} + + e.load_filtered_policy(filter) + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + self.assertFalse(e.enforce("bob", "data1", "read")) + self.assertFalse(e.enforce("bob", "data1", "write")) + self.assertFalse(e.enforce("bob", "data2", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + def test_str(self): """ test __str__ function