diff --git a/.gitignore b/.gitignore index 894a44cc..188deb51 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ venv.bak/ # mypy .mypy_cache/ +_trial_temp +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..5dba5b86 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python + python: + - "3.6" + - "3.7" + script: + - python -m unittest discover -s tests -t tests + deploy: + provider: pypi + user: "techlee" + password: + secure: "Your encrypted password" + on: + tags: true \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..d11408c2 --- /dev/null +++ b/README.md @@ -0,0 +1,297 @@ +PyCasbin +==== + +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/casbin/lobby) +[![Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg)](http://www.patreon.com/yangluo) + +**News**: still worry about how to write the correct Casbin policy? ``Casbin online editor`` is coming to help! Try it at: http://casbin.org/editor/ + +Casbin is a powerful and efficient open-source access control library for Golang projects. It provides support for enforcing authorization based on various [access control models](https://en.wikipedia.org/wiki/Computer_security_model). + +## All the languages supported by Casbin: + +[![golang](https://casbin.org/docs/assets/langs/golang.png)](https://github.com/casbin/casbin) | [![java](https://casbin.org/docs/assets/langs/java.png)](https://github.com/casbin/jcasbin) | [![nodejs](https://casbin.org/docs/assets/langs/nodejs.png)](https://github.com/casbin/node-casbin) | [![php](https://casbin.org/docs/assets/langs/php.png)](https://github.com/php-casbin/php-casbin) | [![php](https://casbin.org/docs/assets/langs/python.png)](https://github.com/casbin/pycasbin) +----|----|----|----|---- +[Casbin](https://github.com/casbin/casbin) | [jCasbin](https://github.com/casbin/jcasbin) | [node-Casbin](https://github.com/casbin/node-casbin) | [PHP-Casbin](https://github.com/php-casbin/php-casbin) | [PyCasbin](https://github.com/casbin/pycasbin) +production-ready | production-ready | production-ready | production-ready | experimental + +## Table of contents + +- [Supported models](#supported-models) +- [How it works?](#how-it-works) +- [Features](#features) +- [Installation](#installation) +- [Documentation](#documentation) +- [Online editor](#online-editor) +- [Tutorials](#tutorials) +- [Get started](#get-started) +- [Policy management](#policy-management) +- [Policy persistence](#policy-persistence) +- [Policy consistence between multiple nodes](#policy-consistence-between-multiple-nodes) +- [Role manager](#role-manager) +- [Multi-threading](#multi-threading) +- [Benchmarks](#benchmarks) +- [Examples](#examples) +- [How to use Casbin as a service?](#how-to-use-casbin-as-a-service) +- [Our adopters](#our-adopters) + +## Supported models + +1. [**ACL (Access Control List)**](https://en.wikipedia.org/wiki/Access_control_list) +2. **ACL with [superuser](https://en.wikipedia.org/wiki/Superuser)** +3. **ACL without users**: especially useful for systems that don't have authentication or user log-ins. +3. **ACL without resources**: some scenarios may target for a type of resources instead of an individual resource by using permissions like ``write-article``, ``read-log``. It doesn't control the access to a specific article or log. +4. **[RBAC (Role-Based Access Control)](https://en.wikipedia.org/wiki/Role-based_access_control)** +5. **RBAC with resource roles**: both users and resources can have roles (or groups) at the same time. +6. **RBAC with domains/tenants**: users can have different role sets for different domains/tenants. +7. **[ABAC (Attribute-Based Access Control)](https://en.wikipedia.org/wiki/Attribute-Based_Access_Control)**: syntax sugar like ``resource.Owner`` can be used to get the attribute for a resource. +8. **[RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer)**: supports paths like ``/res/*``, ``/res/:id`` and HTTP methods like ``GET``, ``POST``, ``PUT``, ``DELETE``. +9. **Deny-override**: both allow and deny authorizations are supported, deny overrides the allow. +10. **Priority**: the policy rules can be prioritized like firewall rules. + +## How it works? + +In Casbin, an access control model is abstracted into a CONF file based on the **PERM metamodel (Policy, Effect, Request, Matchers)**. So switching or upgrading the authorization mechanism for a project is just as simple as modifying a configuration. You can customize your own access control model by combining the available models. For example, you can get RBAC roles and ABAC attributes together inside one model and share one set of policy rules. + +The most basic and simplest model in Casbin is ACL. ACL's model CONF is: + +```ini +# Request definition +[request_definition] +r = sub, obj, act + +# Policy definition +[policy_definition] +p = sub, obj, act + +# Policy effect +[policy_effect] +e = some(where (p.eft == allow)) + +# Matchers +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act + +``` + +An example policy for ACL model is like: + +``` +p, alice, data1, read +p, bob, data2, write +``` + +It means: + +- alice can read data1 +- bob can write data2 + +We also support multi-line mode by appending '\\' in the end: + +```ini +# Matchers +[matchers] +m = r.sub == p.sub && r.obj == p.obj \ + && r.act == p.act +``` + +Further more, if you are using ABAC, you can try operator `in` like following in Casbin **golang** edition (jCasbin and Node-Casbin are not supported yet): + +```ini +# Matchers +[matchers] +m = r.obj == p.obj && r.act == p.act || r.obj in ('data2', 'data3') +``` + +But you **SHOULD** make sure that the length of the array is **MORE** than **1**, otherwise there will cause it to panic. + +For more operators, you may take a look at [govaluate](https://github.com/Knetic/govaluate) + +## Features + +What Casbin does: + +1. enforce the policy in the classic ``{subject, object, action}`` form or a customized form as you defined, both allow and deny authorizations are supported. +2. handle the storage of the access control model and its policy. +3. manage the role-user mappings and role-role mappings (aka role hierarchy in RBAC). +4. support built-in superuser like ``root`` or ``administrator``. A superuser can do anything without explict permissions. +5. multiple built-in operators to support the rule matching. For example, ``keyMatch`` can map a resource key ``/foo/bar`` to the pattern ``/foo*``. + +What Casbin does NOT do: + +1. authentication (aka verify ``username`` and ``password`` when a user logs in) +2. manage the list of users or roles. I believe it's more convenient for the project itself to manage these entities. Users usually have their passwords, and Casbin is not designed as a password container. However, Casbin stores the user-role mapping for the RBAC scenario. + +## Installation + +``` +pip install casbin +``` + +## Documentation + +https://casbin.org/docs/en/overview + +## Online editor + +You can also use the online editor (http://casbin.org/editor/) to write your Casbin model and policy in your web browser. It provides functionality such as ``syntax highlighting`` and ``code completion``, just like an IDE for a programming language. + +## Tutorials + +https://casbin.org/docs/en/tutorials + +## Get started + +1. New a Casbin enforcer with a model file and a policy file: + +```python +import casbin +e = casbin.Enforcer("path/to/model.conf", "path/to/policy.csv") +``` + +Note: you can also initialize an enforcer with policy in DB instead of file, see [Persistence](#persistence) section for details. + +2. Add an enforcement hook into your code right before the access happens: + +```python +sub = "alice" # the user that wants to access a resource. +obj = "data1" # the resource that is going to be accessed. +act = "read" # the operation that the user performs on the resource. + +if e.enforce(sub, obj, act): + # permit alice to read data1 + pass +else: + # deny the request, show an error + pass +``` + +3. Besides the static policy file, Casbin also provides API for permission management at run-time. For example, You can get all the roles assigned to a user as below: + +```python +roles = e.get_roles("alice") +``` + +See [Policy management APIs](#policy-management) for more usage. + +4. Please refer to the ``tests`` files for more usage. + +## Policy management + +Casbin provides two sets of APIs to manage permissions: + +- [Management API](https://github.com/casbin/casbin/blob/master/management_api.go): the primitive API that provides full support for Casbin policy management. See [here](https://github.com/casbin/casbin/blob/master/management_api_test.go) for examples. +- [RBAC API](https://github.com/casbin/casbin/blob/master/rbac_api.go): a more friendly API for RBAC. This API is a subset of Management API. The RBAC users could use this API to simplify the code. See [here](https://github.com/casbin/casbin/blob/master/rbac_api_test.go) for examples. + +We also provide a web-based UI for model management and policy management: + +![model editor](https://hsluoyz.github.io/casbin/ui_model_editor.png) + +![policy editor](https://hsluoyz.github.io/casbin/ui_policy_editor.png) + +## Policy persistence + +In Casbin, the policy storage is implemented as an adapter (aka middleware for Casbin). To keep light-weight, we don't put adapter code in the main library (except the default file adapter). A complete list of Casbin adapters is provided as below. Any 3rd-party contribution on a new adapter is welcomed, please inform us and I will put it in this list:) + +Adapter | Type | Author | Description +----|------|----|---- +[File Adapter (built-in)](https://casbin.org/docs/en/policy-storage#file-adapter-built-in) | File | Casbin | Persistence for [.CSV (Comma-Separated Values)](https://en.wikipedia.org/wiki/Comma-separated_values) files +[Filtered File Adapter (built-in)](https://github.com/casbin/casbin#policy-enforcement-at-scale) | File | [@faceless-saint](https://github.com/faceless-saint) | Persistence for [.CSV (Comma-Separated Values)](https://en.wikipedia.org/wiki/Comma-separated_values) files with policy subset loading support + +For details of adapters, please refer to the documentation: https://casbin.org/docs/en/policy-storage + +## 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. + + +## 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. + +Similar to policy storage adapters, we don't put watcher code in the main library. Any support for a new messaging system should be implemented as a watcher. A complete list of Casbin watchers is provided as below. Any 3rd-party contribution on a new watcher is welcomed, please inform us and I will put it in this list:) + +Watcher | Type | Author | Description +----|------|----|---- + +## Role manager + +The role manager is used to manage the RBAC role hierarchy (user-role mapping) in Casbin. A role manager can retrieve the role data from Casbin policy rules or external sources such as LDAP, Okta, Auth0, Azure AD, etc. We support different implementations of a role manager. To keep light-weight, we don't put role manager code in the main library (except the default role manager). A complete list of Casbin role managers is provided as below. Any 3rd-party contribution on a new role manager is welcomed, please inform us and I will put it in this list:) + +Role manager | Author | Description +----|----|---- +[Default Role Manager (built-in)](https://github.com/casbin/casbin/blob/master/casbin/rbac/default_role_manager/role_manager.py) | Casbin | Supports role hierarchy stored in Casbin policy + +For developers: all role managers must implement the [RoleManager](https://github.com/casbin/casbin/blob/master/casbin/rbac/role_manager.py) interface. + +## Multi-threading + +If you use Casbin in a multi-threading manner, you can use the synchronized wrapper of the Casbin enforcer: https://github.com/casbin/casbin/blob/master/enforcer_synced.go. + +It also supports the ``AutoLoad`` feature, which means the Casbin enforcer will automatically load the latest policy rules from DB if it has changed. Call ``StartAutoLoadPolicy()`` to start automatically loading policy periodically and call ``StopAutoLoadPolicy()`` to stop it. + +## Benchmarks + +The overhead of policy enforcement is benchmarked in [model_b_test.go](https://github.com/casbin/casbin/blob/master/model_b_test.go). The testbed is: + +``` +Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz, 2601 Mhz, 4 Core(s), 8 Logical Processor(s) +``` + +The benchmarking result of ``go test -bench=. -benchmem`` is as follows (op = an ``Enforce()`` call, ms = millisecond, KB = kilo bytes): + +Test case | Size | Time overhead | Memory overhead +----|------|------|---- +ACL | 2 rules (2 users) | 0.015493 ms/op | 5.649 KB +RBAC | 5 rules (2 users, 1 role) | 0.021738 ms/op | 7.522 KB +RBAC (small) | 1100 rules (1000 users, 100 roles) | 0.164309 ms/op | 80.620 KB +RBAC (medium) | 11000 rules (10000 users, 1000 roles) | 2.258262 ms/op | 765.152 KB +RBAC (large) | 110000 rules (100000 users, 10000 roles) | 23.916776 ms/op | 7.606 MB +RBAC with resource roles | 6 rules (2 users, 2 roles) | 0.021146 ms/op | 7.906 KB +RBAC with domains/tenants | 6 rules (2 users, 1 role, 2 domains) | 0.032696 ms/op | 10.755 KB +ABAC | 0 rule (0 user) | 0.007510 ms/op | 2.328 KB +RESTful | 5 rules (3 users) | 0.045398 ms/op | 91.774 KB +Deny-override | 6 rules (2 users, 1 role) | 0.023281 ms/op | 8.370 KB +Priority | 9 rules (2 users, 2 roles) | 0.016389 ms/op | 5.313 KB + +## Examples + +Model | Model file | Policy file +----|------|---- +ACL | [basic_model.conf](https://github.com/casbin/casbin/blob/master/examples/basic_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin/blob/master/examples/basic_policy.csv) +ACL with superuser | [basic_model_with_root.conf](https://github.com/casbin/casbin/blob/master/examples/basic_with_root_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin/blob/master/examples/basic_policy.csv) +ACL without users | [basic_model_without_users.conf](https://github.com/casbin/casbin/blob/master/examples/basic_without_users_model.conf) | [basic_policy_without_users.csv](https://github.com/casbin/casbin/blob/master/examples/basic_without_users_policy.csv) +ACL without resources | [basic_model_without_resources.conf](https://github.com/casbin/casbin/blob/master/examples/basic_without_resources_model.conf) | [basic_policy_without_resources.csv](https://github.com/casbin/casbin/blob/master/examples/basic_without_resources_policy.csv) +RBAC | [rbac_model.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_model.conf) | [rbac_policy.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_policy.csv) +RBAC with resource roles | [rbac_model_with_resource_roles.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_resource_roles_model.conf) | [rbac_policy_with_resource_roles.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_resource_roles_policy.csv) +RBAC with domains/tenants | [rbac_model_with_domains.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_model.conf) | [rbac_policy_with_domains.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_policy.csv) +ABAC | [abac_model.conf](https://github.com/casbin/casbin/blob/master/examples/abac_model.conf) | N/A +RESTful | [keymatch_model.conf](https://github.com/casbin/casbin/blob/master/examples/keymatch_model.conf) | [keymatch_policy.csv](https://github.com/casbin/casbin/blob/master/examples/keymatch_policy.csv) +Deny-override | [rbac_model_with_deny.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_deny_model.conf) | [rbac_policy_with_deny.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_deny_policy.csv) +Priority | [priority_model.conf](https://github.com/casbin/casbin/blob/master/examples/priority_model.conf) | [priority_policy.csv](https://github.com/casbin/casbin/blob/master/examples/priority_policy.csv) + +## How to use Casbin as a service? + +- [Casbin Server](https://github.com/casbin/casbin-server): The official ``Casbin as a Service`` solution based on [gRPC](https://grpc.io/), both Management API and RBAC API are provided. +- [Go-Simple-API-Gateway](https://github.com/Soontao/go-simple-api-gateway): A simple API gateway written by golang, supports for authentication and authorization. +- [middleware-acl](https://github.com/luk4z7/middleware-acl): RESTful access control middleware based on Casbin. + +## Our adopters + +### Web frameworks + +... + + +## License + +This project is licensed under the [Apache 2.0 license](LICENSE). + +## Contact + +If you have any issues or feature requests, please contact us. PR is welcomed. +- https://github.com/casbin/pycasbin/issues +- techlee@qq.com +- Tencent QQ group: [546057381](//shang.qq.com/wpa/qunwpa?idkey=8ac8b91fc97ace3d383d0035f7aa06f7d670fd8e8d4837347354a31c18fac885) \ No newline at end of file diff --git a/casbin/__init__.py b/casbin/__init__.py new file mode 100644 index 00000000..0f51c07b --- /dev/null +++ b/casbin/__init__.py @@ -0,0 +1 @@ +from .enforcer import * diff --git a/casbin/config/__init__.py b/casbin/config/__init__.py new file mode 100644 index 00000000..3558f420 --- /dev/null +++ b/casbin/config/__init__.py @@ -0,0 +1 @@ +from .config import Config \ No newline at end of file diff --git a/casbin/config/config.py b/casbin/config/config.py new file mode 100644 index 00000000..6ea22df6 --- /dev/null +++ b/casbin/config/config.py @@ -0,0 +1,127 @@ +from io import StringIO + + +class Config: + """represents an implementation of the ConfigInterface""" + + # DEFAULT_SECTION specifies the name of a section if no name provided + DEFAULT_SECTION = 'default' + # DEFAULT_COMMENT defines what character(s) indicate a comment `#` + DEFAULT_COMMENT = '#' + # DEFAULT_COMMENT_SEM defines what alternate character(s) indicate a comment `;` + DEFAULT_COMMENT_SEM = ';' + # DEFAULT_MULTI_LINE_SEPARATOR defines what character indicates a multi-line content + DEFAULT_MULTI_LINE_SEPARATOR = '\\' + + _data = dict() + + def __init__(self): + self._data = dict() + + @staticmethod + def new_config(conf_name): + c = Config() + c._parse(conf_name) + return c + + @staticmethod + def new_config_from_text(text): + c = Config() + f = StringIO(text) + c._parse_buffer(f) + return c + + def add_config(self, section, option, value): + if section == '': + section = self.DEFAULT_SECTION + + if section not in self._data.keys(): + self._data[section] = {} + + self._data[section][option] = value + + def _parse(self, fname): + with open(fname, 'rb') as f: + self._parse_buffer(f) + + def _parse_buffer(self, f): + section = '' + line_num = 0 + buf = [] + can_write = False + while True: + if can_write: + self._write(section, line_num, buf) + can_write = False + line_num = line_num + 1 + + line = f.readline() + + if not line: + if len(buf) > 0: + self._write(section, line_num, buf) + break + line = line.strip().decode() + + if '' == line or self.DEFAULT_COMMENT == line[0:1] or self.DEFAULT_COMMENT_SEM == line[0:1]: + can_write = True + continue + elif '[' == line[0:1] and ']' == line[-1]: + if len(buf) > 0: + self._write(section, line_num, buf) + can_write = False + section = line[1:-1] + else: + p = '' + if self.DEFAULT_MULTI_LINE_SEPARATOR == line[-1]: + p = line[0:-1].strip() + else: + p = line + can_write = True + buf.append(p) + + def _write(self, section, line_num, b): + + buf = "".join(b) + if len(buf) <= 0: + return + option_val = buf.split('=', 1) + + if len(option_val) != 2: + raise RuntimeError('parse the content error : line {} , {} = ?'.format(line_num, option_val[0])) + + option = option_val[0].strip() + value = option_val[1].strip() + + self.add_config(section, option, value) + + del b[:] + + def set(self, key, value): + if len(key) == 0: + raise RuntimeError("key is empty") + + keys = key.lower().split('::') + if len(keys) >= 2: + section = keys[0] + option = keys[1] + else: + section = "" + option = keys[0] + self.add_config(section, option, value) + + def get(self, key): + """section.key or key""" + + keys = key.lower().split('::') + if len(keys) >= 2: + section = keys[0] + option = keys[1] + else: + section = self.DEFAULT_SECTION + option = keys[0] + + if section in self._data.keys(): + if option in self._data[section].keys(): + return self._data[section][option] + return '' diff --git a/casbin/effect/__init__.py b/casbin/effect/__init__.py new file mode 100644 index 00000000..5adfc358 --- /dev/null +++ b/casbin/effect/__init__.py @@ -0,0 +1,2 @@ +from .default_effector import DefaultEffector +from .effector import Effector diff --git a/casbin/effect/default_effector.py b/casbin/effect/default_effector.py new file mode 100644 index 00000000..edc7a634 --- /dev/null +++ b/casbin/effect/default_effector.py @@ -0,0 +1,44 @@ +from .effector import Effector + + +class DefaultEffector(Effector): + """default effector for Casbin.""" + + def merge_effects(self, expr, effects, results): + """merges all matching results collected by the enforcer into a single decision.""" + + result = False + if expr == "some(where (p_eft == allow))": + for eft in effects: + if eft == self.ALLOW: + result = True + break + + elif expr == "!some(where (p_eft == deny))": + result = True + + for eft in effects: + if eft == self.DENY: + result = False + break + + elif expr == "some(where (p_eft == allow)) && !some(where (p_eft == deny))": + for eft in effects: + if eft == self.ALLOW: + result = True + elif eft == self.DENY: + result = False + break + + elif expr == "priority(p_eft) || deny": + for eft in effects: + if eft != self.INDETERMINATE: + if eft == self.ALLOW: + result = True + else: + result = False + break + else: + raise RuntimeError("unsupported effect") + + return result diff --git a/casbin/effect/effector.py b/casbin/effect/effector.py new file mode 100644 index 00000000..c9501acd --- /dev/null +++ b/casbin/effect/effector.py @@ -0,0 +1,12 @@ +class Effector: + """Effector is the interface for Casbin effectors.""" + + ALLOW = 0 + + INDETERMINATE = 1 + + DENY = 2 + + def merge_effects(self, expr, effects, results): + """merges all matching results collected by the enforcer into a single decision.""" + pass diff --git a/casbin/enforcer.py b/casbin/enforcer.py new file mode 100644 index 00000000..68df8f93 --- /dev/null +++ b/casbin/enforcer.py @@ -0,0 +1,294 @@ +from casbin import log +from casbin.persist.adapters import FileAdapter +from casbin.model import Model, FunctionMap +from casbin.rbac import default_role_manager +from casbin.util import generate_g_function, expression +from casbin.effect import DefaultEffector, Effector + + +class Enforcer: + """creates an enforcer via file or DB. + + Uses: + File: + e = casbin.Enforcer("path/to/basic_model.conf", "path/to/basic_policy.csv") + MySQL DB: + a = mysqladapter.DBAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/") + e = casbin.NewEnforcer("path/to/basic_model.conf", a) + """ + + model_path = "" + model = None + fm = None + eft = None + + adapter = None + watcher = None + rm = None + + enabled = False + auto_save = False + auto_build_role_links = False + + def __init__(self, model=None, adapter=None, enable_log=False): + self.enable_log(enable_log) + if isinstance(model, str): + if isinstance(adapter, str): + self.init_with_file(model, adapter) + else: + self.init_with_adapter(model, adapter) + pass + else: + if isinstance(adapter, str): + return RuntimeError("Invalid parameters for enforcer.") + else: + self.init_with_model_and_adapter(model, adapter) + + def init_with_file(self, model_path, policy_path): + """initializes an enforcer with a model file and a policy file.""" + a = FileAdapter(policy_path) + self.init_with_adapter(model_path, a) + + def init_with_adapter(self, model_path, adapter=None): + """initializes an enforcer with a database adapter.""" + m = self.new_model(model_path) + self.init_with_model_and_adapter(m, adapter) + + self.model_path = model_path + + def init_with_model_and_adapter(self, m, adapter=None): + """initializes an enforcer with a model and a database adapter.""" + self.adapter = adapter + + self.model = m + self.model.print_model() + self.fm = FunctionMap.load_function_map() + + self._initialize() + + # Do not initialize the full policy when using a filtered adapter + if self.adapter: + self.load_policy() + + def _initialize(self): + self.rm = default_role_manager.RoleManager(10) + self.eft = DefaultEffector() + self.watcher = None + + self.enabled = True + self.auto_save = True + self.auto_build_role_links = True + + @staticmethod + def new_model(path="", text=""): + """creates a model.""" + + m = Model() + if len(path) > 0: + m.load_model(path) + else: + m.load_model_from_text(text) + + return m + + def load_model(self): + """reloads the model from the model CONF file. + Because the policy is attached to a model, so the policy is invalidated and needs to be reloaded by calling LoadPolicy(). + """ + + self.model = self.new_model() + self.model.load_model(self.model_path) + self.model.print_model() + self.fm = FunctionMap.load_function_map() + + def get_model(self): + """gets the current model.""" + + return self.model + + def set_model(self, m): + """sets the current model.""" + + self.model = m + self.fm = FunctionMap.load_function_map() + + def get_adapter(self): + """gets the current adapter.""" + + return self.adapter + + def set_adapter(self, adapter): + """sets the current adapter.""" + + self.adapter = adapter + + def set_watcher(self, watcher): + """sets the current watcher.""" + + self.watcher = watcher + pass + + def set_role_manager(self, rm): + """sets the current role manager.""" + + self.rm = rm + + def set_effector(self, eft): + """sets the current effector.""" + + self.eft = eft + + def clear_policy(self): + """ clears all policy.""" + + self.model.clear_policy() + + def load_policy(self): + """reloads the policy from file/database.""" + + self.model.clear_policy() + self.adapter.load_policy(self.model) + + self.model.print_policy() + if self.auto_build_role_links: + self.build_role_links() + + def load_filtered_policy(self, filter): + """reloads a filtered policy from file/database.""" + + pass + + def is_filtered(self): + """returns true if the loaded policy has been filtered.""" + + pass + + def save_policy(self): + if self.is_filtered(): + return RuntimeError("cannot save a filtered policy") + + self.adapter.save_policy(self.model) + + if self.watcher: + self.watcher.update() + + def enable_enforce(self, enabled=True): + """changes the enforcing state of Casbin, + when Casbin is disabled, all access will be allowed by the Enforce() function. + """ + + self.enabled = enabled + + def enable_log(self, enable): + """changes whether Casbin will log messages to the Logger.""" + + log.get_logger().enable_log(enable) + + def enable_auto_save(self, auto_save): + """controls whether to save a policy rule automatically to the adapter when it is added or removed.""" + self.auto_save = auto_save + + def enable_auto_build_role_links(self, auto_build_role_links): + """controls whether to rebuild the role inheritance relations when a role is added or deleted.""" + self.auto_build_role_links = auto_build_role_links + + def build_role_links(self): + """manually rebuild the role inheritance relations.""" + + self.rm.clear() + self.model.build_role_links(self.rm) + + def enforce(self, *rvals): + """decides whether a "subject" can access a "object" with the operation "action", + input parameters are usually: (sub, obj, act). + """ + + if not self.enabled: + return False + + functions = {} + for key, val in self.fm.get_functions().items(): + functions[key] = val + + if "g" in self.model.model.keys(): + for key, ast in self.model.model["g"].items(): + rm = ast.rm + functions[key] = generate_g_function(rm) + + if "m" not in self.model.model.keys(): + return RuntimeError("model is undefined") + + if "m" not in self.model.model["m"].keys(): + return RuntimeError("model is undefined") + + exp_string = self.model.model["m"]["m"].value + + policy_effects = [] + matcher_results = [] + + policy_len = len(self.model.model["p"]["p"].policy) + + if not 0 == policy_len: + for i, pvals in enumerate(self.model.model["p"]["p"].policy): + parameters = dict() + for j, token in enumerate(self.model.model["r"]["r"].tokens): + parameters[token] = rvals[j] + + for j, token in enumerate(self.model.model["p"]["p"].tokens): + parameters[token] = pvals[j] + + result = expression.evaluate(exp_string, parameters, functions) + + if isinstance(result, bool): + if not result: + policy_effects.append(Effector.INDETERMINATE) + continue + elif isinstance(result, float): + if 0 == result: + policy_effects.append(Effector.INDETERMINATE) + continue + else: + matcher_results.append(result) + else: + raise RuntimeError("matcher result should be bool, int or float") + + if "p_eft" in parameters.keys(): + eft = parameters["p_eft"] + if "allow" == eft: + policy_effects.append(Effector.ALLOW) + elif "deny" == eft: + policy_effects.append(Effector.DENY) + else: + policy_effects.append(Effector.INDETERMINATE) + else: + policy_effects.append(Effector.ALLOW) + + if "priority(p_eft) || deny" == self.model.model["e"]["e"].value: + break + + else: + parameters = dict() + for j, token in enumerate(self.model.model["r"]["r"].tokens): + parameters[token] = rvals[j] + + for token in enumerate(self.model.model["p"]["p"].tokens): + parameters[token] = "" + + result = expression.evaluate(exp_string, parameters, functions) + + if result: + policy_effects.append(Effector.ALLOW) + else: + policy_effects.append(Effector.INDETERMINATE) + + result = self.eft.merge_effects(self.model.model["e"]["e"].value, policy_effects, matcher_results) + + # Log request. + if log.get_logger().is_enabled(): + req_str = "Request: " + req_str = req_str + ", ".join(rvals) + + req_str = req_str + " ---> %s" % result + log.log_print(req_str) + + return result diff --git a/casbin/log/__init__.py b/casbin/log/__init__.py new file mode 100644 index 00000000..498b1645 --- /dev/null +++ b/casbin/log/__init__.py @@ -0,0 +1,3 @@ +from .default_logger import DefaultLogger +from .logger import Logger +from .log import * diff --git a/casbin/log/default_logger.py b/casbin/log/default_logger.py new file mode 100644 index 00000000..ad421565 --- /dev/null +++ b/casbin/log/default_logger.py @@ -0,0 +1,31 @@ +from .logger import Logger +import logging + +logging.basicConfig(level=logging.NOTSET, format="%(asctime)s - %(levelname)s - %(message)s") + + +class DefaultLogger(Logger): + """the implementation for a Logger using logging.""" + + enable = False + + def enable_log(self, enable): + """controls whether print the message.""" + self.enable = enable + + def is_enabled(self): + """returns if logger is enabled.""" + return self.enable + + def write(self, *v): + """formats using the default formats for its operands and logs the message.""" + if self.enable: + s = "" + for vv in v: + s = s + str(vv) + logging.info(s) + + def writef(self, fmt, *v): + """formats according to a format specifier and logs the message.""" + if self.enable: + logging.info(fmt, *v) diff --git a/casbin/log/log.py b/casbin/log/log.py new file mode 100644 index 00000000..9acb70a6 --- /dev/null +++ b/casbin/log/log.py @@ -0,0 +1,23 @@ +from .default_logger import DefaultLogger + +logger = DefaultLogger() + + +def set_logger(l): + """sets the current logger.""" + logger = l + + +def get_logger(): + """returns the current logger.""" + return logger + + +def log_print(*v): + """prints the log.""" + logger.write(*v) + + +def log_printf(fmt, *v): + """prints the log with the format.""" + logger.writef(fmt, *v) diff --git a/casbin/log/logger.py b/casbin/log/logger.py new file mode 100644 index 00000000..2624ea80 --- /dev/null +++ b/casbin/log/logger.py @@ -0,0 +1,18 @@ +class Logger: + """Logger is the logging interface implementation.""" + + def enable_log(self, enable): + """controls whether print the message.""" + pass + + def is_enabled(self): + """returns if logger is enabled.""" + pass + + def write(self, *v): + """formats using the default formats for its operands and logs the message.""" + pass + + def writef(self, fmt, *v): + """formats according to a format specifier and logs the message.""" + pass diff --git a/casbin/model/__init__.py b/casbin/model/__init__.py new file mode 100644 index 00000000..2bdda40b --- /dev/null +++ b/casbin/model/__init__.py @@ -0,0 +1,4 @@ +from .assertion import Assertion +from .model import Model +from .policy import Policy +from .function import FunctionMap diff --git a/casbin/model/assertion.py b/casbin/model/assertion.py new file mode 100644 index 00000000..f7253a30 --- /dev/null +++ b/casbin/model/assertion.py @@ -0,0 +1,30 @@ +from casbin import log + + +class Assertion: + key = "" + value = "" + tokens = [] + policy = [] + rm = None + + def build_role_links(self, rm): + self.rm = rm + count = self.value.count("_") + + for rule in self.policy: + if count < 2: + raise RuntimeError('the number of "_" in role definition should be at least 2') + + if len(rule) < count: + raise RuntimeError("grouping policy elements do not meet role definition") + + if count == 2: + self.rm.add_link(rule[0], rule[1]) + elif count == 3: + self.rm.add_link(rule[0], rule[1], rule[2]) + elif count == 4: + self.rm.add_link(rule[0], rule[1], rule[2], rule[3]) + + log.log_print("Role links for: " + self.key) + self.rm.print_roles() diff --git a/casbin/model/function.py b/casbin/model/function.py new file mode 100644 index 00000000..50fd4b47 --- /dev/null +++ b/casbin/model/function.py @@ -0,0 +1,21 @@ +from casbin import util + + +class FunctionMap: + fm = dict() + + def add_function(self, name, func): + self.fm[name] = func + + @staticmethod + def load_function_map(): + fm = FunctionMap() + fm.add_function("keyMatch", util.key_match_func) + fm.add_function("keyMatch2", util.key_match2_func) + fm.add_function("regexMatch", util.regex_match_func) + fm.add_function("ipMatch", util.ip_match_func) + + return fm + + def get_functions(self): + return self.fm diff --git a/casbin/model/model.py b/casbin/model/model.py new file mode 100644 index 00000000..3c5c6b1f --- /dev/null +++ b/casbin/model/model.py @@ -0,0 +1,82 @@ +from . import Assertion +from casbin import util, config, log +from .policy import Policy + + +class Model(Policy): + model = dict() + + section_name_map = { + 'r': 'request_definition', + 'p': 'policy_definition', + 'g': 'role_definition', + 'e': 'policy_effect', + 'm': 'matchers', + } + + def _load_assertion(self, cfg, sec, key): + value = cfg.get(self.section_name_map[sec] + "::" + key) + + return self.add_def(sec, key, value) + + def add_def(self, sec, key, value): + if value == "": + return + + ast = Assertion() + ast.key = key + ast.value = value + + if "r" == sec or "p" == sec: + ast.tokens = ast.value.split(", ") + for i,token in enumerate(ast.tokens): + ast.tokens[i] = key + "_" + token + else: + ast.value = util.remove_comments(util.escape_assertion(ast.value)) + + if sec not in self.model.keys(): + self.model[sec] = {} + + self.model[sec][key] = ast + + return True + + def _get_key_suffix(self, i): + if i == 1: + return "" + + return str(i) + + def _load_section(self, cfg, sec): + i = 1 + while True: + if not self._load_assertion(cfg, sec, sec + self._get_key_suffix(i)): + break + else: + i = i + 1 + + def load_model(self, path): + cfg = config.Config.new_config(path) + + self._load_section(cfg, "r") + self._load_section(cfg, "p") + self._load_section(cfg, "e") + self._load_section(cfg, "m") + + self._load_section(cfg, "g") + + def load_model_from_text(self, text): + cfg = config.Config.new_config_from_text(text) + + self._load_section(cfg, "r") + self._load_section(cfg, "p") + self._load_section(cfg, "e") + self._load_section(cfg, "m") + + self._load_section(cfg, "g") + + def print_model(self): + log.log_print("Model:") + for k, v in self.model.items(): + for i, j in v.items(): + log.log_printf("%s.%s: %s", k, i, j.value) diff --git a/casbin/model/policy.py b/casbin/model/policy.py new file mode 100644 index 00000000..a120b395 --- /dev/null +++ b/casbin/model/policy.py @@ -0,0 +1,32 @@ +from casbin import log + + +class Policy: + model = dict() + + def build_role_links(self, rm): + if "g" not in self.model.keys(): + return + + for ast in self.model["g"].values(): + ast.build_role_links(rm) + + def print_policy(self): + log.log_print("Policy:") + for sec in ["p", "g"]: + if sec not in self.model.keys(): + continue + + for key, ast in self.model[sec].items(): + log.log_print(key, ": ", ast.value, ": ", ast.policy) + + def clear_policy(self): + for sec in ["p", "g"]: + if sec not in self.model.keys(): + continue + + for key, ast in self.model[sec].items(): + self.model[sec][key].policy = [] + + def get_policy(self, sec, ptype): + return self.model[sec][ptype].policy diff --git a/casbin/persist/__init__.py b/casbin/persist/__init__.py new file mode 100644 index 00000000..38fc8322 --- /dev/null +++ b/casbin/persist/__init__.py @@ -0,0 +1 @@ +from .adapter import * diff --git a/casbin/persist/adapter.py b/casbin/persist/adapter.py new file mode 100644 index 00000000..6588866d --- /dev/null +++ b/casbin/persist/adapter.py @@ -0,0 +1,46 @@ +def load_policy_line(line, model): + """loads a text line as a policy rule to model.""" + + if line == "": + return + + if line[:1] == "#": + return + + tokens = line.split(", ") + key = tokens[0] + sec = key[0] + + if sec not in model.model.keys(): + return + + if key not in model.model[sec].keys(): + return + + model.model[sec][key].policy.append(tokens[1:]) + + +class Adapter: + """the interface for Casbin adapters.""" + + def load_policy(self, model): + """loads all policy rules from the storage.""" + pass + + def save_policy(self, model): + """saves all policy rules to the storage.""" + pass + + def save_policy(self, model): + """adds a policy rule to the storage.""" + pass + + def remove_policy(self, sec, ptype, rule): + """removes a policy rule from the storage.""" + pass + + def remove_filtered_policy(self, sec, ptype, field_index, *field_values): + """removes policy rules that match the filter from the storage. + This is part of the Auto-Save feature. + """ + pass diff --git a/casbin/persist/adapters/__init__.py b/casbin/persist/adapters/__init__.py new file mode 100644 index 00000000..b82b676d --- /dev/null +++ b/casbin/persist/adapters/__init__.py @@ -0,0 +1 @@ +from .file_adapter import FileAdapter diff --git a/casbin/persist/adapters/file_adapter.py b/casbin/persist/adapters/file_adapter.py new file mode 100644 index 00000000..d2a2079b --- /dev/null +++ b/casbin/persist/adapters/file_adapter.py @@ -0,0 +1,41 @@ +from casbin import persist +import os + + +class FileAdapter(persist.Adapter): + """the file adapter for Casbin. + It can load policy from file or save policy to file. + """ + + _file_path = "" + + def __init__(self, file_path): + self._file_path = file_path + + def load_policy(self, model): + if not os.path.isfile(self._file_path): + raise RuntimeError("invalid file path, file path cannot be empty") + + self._load_policy_file(model) + + def save_policy(self, model): + pass + + def _load_policy_file(self, model): + with open(self._file_path, "rb") as file: + line = file.readline() + while line: + persist.load_policy_line(line.decode().strip(), model) + line = file.readline() + + def _save_policy_file(self, text): + pass + + def add_policy(self, sec, ptype, rule): + pass + + def remove_policy(self, sec, ptype, rule): + pass + + def remove_filtered_policy(self, sec, ptype, field_index, *field_values): + pass diff --git a/casbin/rbac/__init__.py b/casbin/rbac/__init__.py new file mode 100644 index 00000000..cd3c1521 --- /dev/null +++ b/casbin/rbac/__init__.py @@ -0,0 +1 @@ +from .role_manager import RoleManager diff --git a/casbin/rbac/default_role_manager/__init__.py b/casbin/rbac/default_role_manager/__init__.py new file mode 100644 index 00000000..4ff0284e --- /dev/null +++ b/casbin/rbac/default_role_manager/__init__.py @@ -0,0 +1 @@ +from .role_manager import RoleManager \ 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 new file mode 100644 index 00000000..b3e4bd6d --- /dev/null +++ b/casbin/rbac/default_role_manager/role_manager.py @@ -0,0 +1,165 @@ +from casbin import log +from casbin.rbac import RoleManager + + +class RoleManager(RoleManager): + """provides a default implementation for the RoleManager interface""" + + all_roles = dict() + max_hierarchy_level = 0 + + def __init__(self, max_hierarchy_level): + self.all_roles = dict() + self.max_hierarchy_level = max_hierarchy_level + + def has_role(self, name): + return name in self.all_roles.keys() + + def create_role(self, name): + if name not in self.all_roles.keys(): + self.all_roles[name] = Role(name) + + return self.all_roles[name] + + def clear(self): + self.all_roles.clear() + + def add_link(self, name1, name2, *domain): + if len(domain) == 1: + name1 = domain[0] + "::" + name1 + name2 = domain[0] + "::" + name2 + elif len(domain) > 1: + return RuntimeError("error: domain should be 1 parameter") + + role1 = self.create_role(name1) + role2 = self.create_role(name2) + role1.add_role(role2) + + def delete_link(self, name1, name2, *domain): + if len(domain) == 1: + name1 = domain[0] + "::" + name1 + name2 = domain[0] + "::" + name2 + elif len(domain) > 1: + return RuntimeError("error: domain should be 1 parameter") + + if not self.has_role(name1) or not self.has_role(name2): + return RuntimeError("error: name1 or name2 does not exist") + + role1 = self.create_role(name1) + role2 = self.create_role(name2) + role1.delete_role(role2) + + def has_link(self, name1, name2, *domain): + if len(domain) == 1: + name1 = domain[0] + "::" + name1 + name2 = domain[0] + "::" + name2 + elif len(domain) > 1: + return RuntimeError("error: domain should be 1 parameter") + + if name1 == name2: + return True + + if not self.has_role(name1) or not self.has_role(name2): + return False + + role1 = self.create_role(name1) + + return role1.has_role(name2, self.max_hierarchy_level) + + def get_roles(self, name, *domain): + if len(domain) == 1: + name = domain[0] + "::" + name + elif len(domain) > 1: + return RuntimeError("error: domain should be 1 parameter") + + if not self.has_role(name): + return dict() + + roles = self.create_role(name).get_roles() + if len(domain) == 1: + for value in roles: + value = value[len(domain[0]) + 2:] + + return roles + + def get_users(self, name, *domain): + if not self.has_role(name): + return RuntimeError("error: name does not exist") + + names = [] + for role in self.all_roles.values(): + if role.has_direct_role(name): + names.append(name) + + return names + + def print_roles(self): + line = [] + for role in self.all_roles.values(): + text = role.to_string() + if text: + line.append(text) + + log.log_print(", ".join(line)) + + +class Role: + """represents the data structure for a role in RBAC.""" + + name = "" + + roles = [] + + def __init__(self, name): + self.name = name + self.roles = [] + + def add_role(self, role): + for rr in self.roles: + if rr.name == role.name: + return + + self.roles.append(role) + + def delete_role(self, role): + for rr in self.roles: + if rr.name == role.name: + self.roles.remove(rr) + return + + def has_role(self, name, hierarchy_level): + if name == self.name: + return True + if hierarchy_level <= 0: + return False + + for role in self.roles: + if role.has_role(name, hierarchy_level - 1): + return True + + return False + + def has_direct_role(self, name): + for role in self.roles: + if role.name == name: + return True + + return False + + def to_string(self): + if len(self.roles) == 0: + return "" + + names = ", ".join(self.get_roles()) + + if len(self.roles) == 1: + return self.name + " < " + names + else: + return self.name + " < (" + names + ")" + + def get_roles(self): + names = [] + for role in self.roles: + names.append(role.name) + + return names diff --git a/casbin/rbac/role_manager.py b/casbin/rbac/role_manager.py new file mode 100644 index 00000000..9c1cefc4 --- /dev/null +++ b/casbin/rbac/role_manager.py @@ -0,0 +1,23 @@ +class RoleManager: + """provides interface to define the operations for managing roles.""" + + def clear(self): + pass + + def add_link(self, name1, name2, *domain): + pass + + def delete_link(self, name1, name2, *domain): + pass + + def has_link(self, name1, name2, *domain): + pass + + def get_roles(self, name, *domain): + pass + + def get_users(self, name, *domain): + pass + + def print_roles(self): + pass diff --git a/casbin/util/__init__.py b/casbin/util/__init__.py new file mode 100644 index 00000000..16ff7acd --- /dev/null +++ b/casbin/util/__init__.py @@ -0,0 +1,3 @@ +from .builtin_operators import * +from .util import * +from .expression import * diff --git a/casbin/util/builtin_operators.py b/casbin/util/builtin_operators.py new file mode 100644 index 00000000..3c7a861e --- /dev/null +++ b/casbin/util/builtin_operators.py @@ -0,0 +1,131 @@ +import re +import ipaddress + + +def key_match(key1, key2): + """determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. + For example, "/foo/bar" matches "/foo/*" + """ + + i = key2.find("*") + if i == -1: + return key1 == key2 + + if len(key1) > i: + return key1[:i] == key2[:i] + return key1 == key2[:i] + + +def key_match_func(*args): + """The wrapper for key_match. + """ + name1 = args[0] + name2 = args[1] + + return key_match(name1, name2) + + +def key_match2(key1, key2): + """determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. + For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource" + """ + + key2 = key2.replace("/*", "/.*") + + pattern = re.compile(r'(.*):[^\/]+(.*)') + while True: + if "/:" not in key2: + break + + key2 = "^" + pattern.sub(r'\g<1>[^\/]+\g<2>', key2, 0) + "$" + + return regex_match(key1, key2) + + +def key_match2_func(*args): + name1 = args[0] + name2 = args[1] + + return key_match2(name1, name2) + + +def key_match3(key1, key2): + """determines determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. + For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/{resource}" + """ + + key2 = key2.replace("/*", "/.*") + + pattern = re.compile(r'(.*){[^\/]+}(.*)') + while True: + if "{" not in key2: + break + + key2 = pattern.sub(r'\g<1>[^\/]+\g<2>', key2, 0) + + return regex_match(key1, key2) + + +def key_match3_func(*args): + name1 = args[0] + name2 = args[1] + + return key_match3(name1, name2) + + +def regex_match(key1, key2): + """determines whether key1 matches the pattern of key2 in regular expression.""" + + res = re.match(key2, key1) + if res: + return True + else: + return False + + +def regex_match_func(*args): + """the wrapper for RegexMatch.""" + + name1 = args[0] + name2 = args[1] + + return regex_match(name1, name2) + + +def ip_match(ip1, ip2): + """IPMatch determines whether IP address ip1 matches the pattern of IP address ip2, ip2 can be an IP address or a CIDR pattern. + For example, "192.168.2.123" matches "192.168.2.0/24" + """ + ip1 = ipaddress.ip_address(ip1) + try: + network = ipaddress.ip_network(ip2, strict=True) + return ip1 in network + except ValueError: + return ip1 == ip2 + + +def ip_match_func(*args): + """the wrapper for IPMatch.""" + + ip1 = args[0] + ip2 = args[1] + + return ip_match(ip1, ip2) + + +def generate_g_function(rm): + """the factory method of the g(_, _) function.""" + + def f(*args): + name1 = args[0] + name2 = args[1] + + if not rm: + return name1 == name2 + elif 2 == len(args): + return rm.has_link(name1, name2) + else: + domain = str(args[2]) + return rm.has_link(name1, name2, domain) + + return f diff --git a/casbin/util/expression.py b/casbin/util/expression.py new file mode 100644 index 00000000..634c3ef8 --- /dev/null +++ b/casbin/util/expression.py @@ -0,0 +1,7 @@ +from simpleeval import simple_eval + + +def evaluate(exp_string, parameters=dict(), functions=dict()): + exp_string = exp_string.replace("&&", "and") + exp_string = exp_string.replace("||", "or") + return simple_eval(exp_string, functions=functions, names=parameters) diff --git a/casbin/util/util.py b/casbin/util/util.py new file mode 100644 index 00000000..e9b13726 --- /dev/null +++ b/casbin/util/util.py @@ -0,0 +1,42 @@ +def escape_assertion(s): + """escapes the dots in the assertion, because the expression evaluation doesn't support such variable names.""" + + s = s.replace("r.", "r_") + s = s.replace("p.", "p_") + + return s + + +def remove_comments(s): + """removes the comments starting with # in the text.""" + + pos = s.find("#") + if pos == -1: + return s + + return s[0:pos].strip() + + +def array_remove_duplicates(s): + """removes any duplicated elements in a string array.""" + found = dict() + j = 0 + for x in s: + if x not in found.keys(): + found[x] = True + s[j] = x + j = j + 1 + + return s[:j] + + +def array_to_string(s): + """gets a printable string for a string array.""" + + return ", ".join(s) + + +def params_to_string(*s): + """gets a printable string for variable number of parameters.""" + + return ", ".join(s) diff --git a/examples/abac_model.conf b/examples/abac_model.conf new file mode 100644 index 00000000..4cfaea38 --- /dev/null +++ b/examples/abac_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == r.obj.Owner \ No newline at end of file diff --git a/examples/basic_inverse_policy.csv b/examples/basic_inverse_policy.csv new file mode 100644 index 00000000..276c4403 --- /dev/null +++ b/examples/basic_inverse_policy.csv @@ -0,0 +1,2 @@ +p, alice, data1, write +p, bob, data2, read \ No newline at end of file diff --git a/examples/basic_model.conf b/examples/basic_model.conf new file mode 100644 index 00000000..dc6da813 --- /dev/null +++ b/examples/basic_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/basic_policy.csv b/examples/basic_policy.csv new file mode 100644 index 00000000..57aaa976 --- /dev/null +++ b/examples/basic_policy.csv @@ -0,0 +1,2 @@ +p, alice, data1, read +p, bob, data2, write \ No newline at end of file diff --git a/examples/basic_with_root_model.conf b/examples/basic_with_root_model.conf new file mode 100644 index 00000000..8f13907e --- /dev/null +++ b/examples/basic_with_root_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root" \ No newline at end of file diff --git a/examples/basic_without_resources_model.conf b/examples/basic_without_resources_model.conf new file mode 100644 index 00000000..f61bd710 --- /dev/null +++ b/examples/basic_without_resources_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, act + +[policy_definition] +p = sub, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.act == p.act \ No newline at end of file diff --git a/examples/basic_without_resources_policy.csv b/examples/basic_without_resources_policy.csv new file mode 100644 index 00000000..c861941b --- /dev/null +++ b/examples/basic_without_resources_policy.csv @@ -0,0 +1,2 @@ +p, alice, read +p, bob, write \ No newline at end of file diff --git a/examples/basic_without_users_model.conf b/examples/basic_without_users_model.conf new file mode 100644 index 00000000..1fe5993c --- /dev/null +++ b/examples/basic_without_users_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = obj, act + +[policy_definition] +p = obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/basic_without_users_policy.csv b/examples/basic_without_users_policy.csv new file mode 100644 index 00000000..79048da6 --- /dev/null +++ b/examples/basic_without_users_policy.csv @@ -0,0 +1,2 @@ +p, data1, read +p, data2, write \ No newline at end of file diff --git a/examples/error/error_model.conf b/examples/error/error_model.conf new file mode 100644 index 00000000..59304dbe --- /dev/null +++ b/examples/error/error_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/error/error_policy.csv b/examples/error/error_policy.csv new file mode 100644 index 00000000..e490c7d2 --- /dev/null +++ b/examples/error/error_policy.csv @@ -0,0 +1,2 @@ +p, alice, data1, read +bob, data2, write \ No newline at end of file diff --git a/examples/ipmatch_model.conf b/examples/ipmatch_model.conf new file mode 100644 index 00000000..26e4b011 --- /dev/null +++ b/examples/ipmatch_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = ipMatch(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/ipmatch_policy.csv b/examples/ipmatch_policy.csv new file mode 100644 index 00000000..ca678a92 --- /dev/null +++ b/examples/ipmatch_policy.csv @@ -0,0 +1,2 @@ +p, 192.168.2.0/24, data1, read +p, 10.0.0.0/16, data2, write \ No newline at end of file diff --git a/examples/keymatch2_model.conf b/examples/keymatch2_model.conf new file mode 100644 index 00000000..944123de --- /dev/null +++ b/examples/keymatch2_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/keymatch2_policy.csv b/examples/keymatch2_policy.csv new file mode 100644 index 00000000..941a48f8 --- /dev/null +++ b/examples/keymatch2_policy.csv @@ -0,0 +1,2 @@ +p, alice, /alice_data/:resource, GET +p, alice, /alice_data2/:id/using/:resId, GET \ No newline at end of file diff --git a/examples/keymatch_custom_model.conf b/examples/keymatch_custom_model.conf new file mode 100644 index 00000000..1cad8bfd --- /dev/null +++ b/examples/keymatch_custom_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatchCustom(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/keymatch_model.conf b/examples/keymatch_model.conf new file mode 100644 index 00000000..4f86ba8f --- /dev/null +++ b/examples/keymatch_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/keymatch_policy.csv b/examples/keymatch_policy.csv new file mode 100644 index 00000000..d6e9b7d4 --- /dev/null +++ b/examples/keymatch_policy.csv @@ -0,0 +1,7 @@ +p, alice, /alice_data/*, GET +p, alice, /alice_data/resource1, POST + +p, bob, /alice_data/resource2, GET +p, bob, /bob_data/*, POST + +p, cathy, /cathy_data, (GET)|(POST) \ No newline at end of file diff --git a/examples/priority_indeterminate_policy.csv b/examples/priority_indeterminate_policy.csv new file mode 100644 index 00000000..c4e67cd9 --- /dev/null +++ b/examples/priority_indeterminate_policy.csv @@ -0,0 +1 @@ +p, alice, data1, read, intdeterminate \ No newline at end of file diff --git a/examples/priority_model.conf b/examples/priority_model.conf new file mode 100644 index 00000000..ece1562e --- /dev/null +++ b/examples/priority_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = priority(p.eft) || deny + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/priority_policy.csv b/examples/priority_policy.csv new file mode 100644 index 00000000..1ec5e5a7 --- /dev/null +++ b/examples/priority_policy.csv @@ -0,0 +1,12 @@ +p, alice, data1, read, allow +p, data1_deny_group, data1, read, deny +p, data1_deny_group, data1, write, deny +p, alice, data1, write, allow + +g, alice, data1_deny_group + +p, data2_allow_group, data2, read, allow +p, bob, data2, read, deny +p, bob, data2, write, deny + +g, bob, data2_allow_group \ No newline at end of file diff --git a/examples/rbac_model.conf b/examples/rbac_model.conf new file mode 100644 index 00000000..71159e38 --- /dev/null +++ b/examples/rbac_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_policy.csv b/examples/rbac_policy.csv new file mode 100644 index 00000000..9bbfa7cf --- /dev/null +++ b/examples/rbac_policy.csv @@ -0,0 +1,6 @@ +p, alice, data1, read +p, bob, data2, write +p, data2_admin, data2, read +p, data2_admin, data2, write + +g, alice, data2_admin \ No newline at end of file diff --git a/examples/rbac_with_deny_model.conf b/examples/rbac_with_deny_model.conf new file mode 100644 index 00000000..33749f00 --- /dev/null +++ b/examples/rbac_with_deny_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_deny_policy.csv b/examples/rbac_with_deny_policy.csv new file mode 100644 index 00000000..0603db8d --- /dev/null +++ b/examples/rbac_with_deny_policy.csv @@ -0,0 +1,7 @@ +p, alice, data1, read, allow +p, bob, data2, write, allow +p, data2_admin, data2, read, allow +p, data2_admin, data2, write, allow +p, alice, data2, write, deny + +g, alice, data2_admin \ No newline at end of file diff --git a/examples/rbac_with_domains_model.conf b/examples/rbac_with_domains_model.conf new file mode 100644 index 00000000..57c37216 --- /dev/null +++ b/examples/rbac_with_domains_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_domains_policy.csv b/examples/rbac_with_domains_policy.csv new file mode 100644 index 00000000..8810a7cc --- /dev/null +++ b/examples/rbac_with_domains_policy.csv @@ -0,0 +1,7 @@ +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 \ No newline at end of file diff --git a/examples/rbac_with_hierarchy_policy.csv b/examples/rbac_with_hierarchy_policy.csv new file mode 100644 index 00000000..f7229986 --- /dev/null +++ b/examples/rbac_with_hierarchy_policy.csv @@ -0,0 +1,10 @@ +p, alice, data1, read +p, bob, data2, write +p, data1_admin, data1, read +p, data1_admin, data1, write +p, data2_admin, data2, read +p, data2_admin, data2, write + +g, alice, admin +g, admin, data1_admin +g, admin, data2_admin \ No newline at end of file diff --git a/examples/rbac_with_not_deny_model.conf b/examples/rbac_with_not_deny_model.conf new file mode 100644 index 00000000..dcf03a1e --- /dev/null +++ b/examples/rbac_with_not_deny_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = !some(where (p_eft == deny)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_resource_roles_model.conf b/examples/rbac_with_resource_roles_model.conf new file mode 100644 index 00000000..845bc6c7 --- /dev/null +++ b/examples/rbac_with_resource_roles_model.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_resource_roles_policy.csv b/examples/rbac_with_resource_roles_policy.csv new file mode 100644 index 00000000..b1d36daf --- /dev/null +++ b/examples/rbac_with_resource_roles_policy.csv @@ -0,0 +1,7 @@ +p, alice, data1, read +p, bob, data2, write +p, data_group_admin, data_group, write + +g, alice, data_group_admin +g2, data1, data_group +g2, data2, data_group \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e8a78a24 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +simpleeval==0.9.8 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..0dbc1da3 --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="casbin", + version="0.1.1", + author="TechLee", + author_email="techlee@qq.com", + description="An authorization library that supports access control models like ACL, RBAC, ABAC in Python", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/casbin/pycasbin", + keywords=["casbin", "rbac", "access control", "abac", "acl", "permission"], + packages=setuptools.find_packages(), + install_requires=['simpleeval>=0.9.8'], + python_requires=">=3.6", + classifiers=[ + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + ], +) diff --git a/tests/config/test.ini b/tests/config/test.ini new file mode 100644 index 00000000..15276c3e --- /dev/null +++ b/tests/config/test.ini @@ -0,0 +1,52 @@ +# test config +debug = true +url = act.wiki + +; redis config +[redis] +redis.key = push1,push2 + +; mysql config +[mysql] +mysql.dev.host = 127.0.0.1 +mysql.dev.user = root +mysql.dev.pass = 123456 +mysql.dev.db = test + +mysql.master.host = 10.0.0.1 +mysql.master.user = root +mysql.master.pass = 89dds)2$#d +mysql.master.db = act + +; math config +[math] +math.i64 = 64 +math.f64 = 64.1 + +; other config +[other] +name = ATC自动化测试^-^&($#……# +key1 = test key + +# multi-line test +[multi1] +name = r.sub==p.sub \ + &&r.obj==p.obj\ + \ +[multi2] +name = r.sub==p.sub \ + &&r.obj==p.obj + +[multi3] +name = r.sub==p.sub \ + &&r.obj==p.obj + +[multi4] +name = \ +\ + \ + +[multi5] +name = r.sub==p.sub \ + &&r.obj==p.obj\ + \ \ No newline at end of file diff --git a/tests/config/test_config.py b/tests/config/test_config.py new file mode 100644 index 00000000..8cfd97ac --- /dev/null +++ b/tests/config/test_config.py @@ -0,0 +1,40 @@ +import os +from casbin.config import Config +from unittest import TestCase + + +class TestConfig(TestCase): + def test_new_config(self): + path = os.path.split(os.path.realpath(__file__))[0] + path = os.path.abspath(path + "/test.ini") + + config = Config.new_config(path) + + # default::key + self.assertEqual(config.get("debug"), "true") + self.assertEqual(config.get("url"), "act.wiki") + + # reids::key + self.assertEqual(config.get("redis::redis.key"), "push1,push2") + self.assertEqual(config.get("mysql::mysql.dev.host"), "127.0.0.1") + self.assertEqual(config.get("mysql::mysql.master.host"), "10.0.0.1") + + # math::key test + self.assertEqual(config.get("math::math.i64"), "64") + self.assertEqual(config.get("math::math.f64"), "64.1") + + # other::key test + self.assertEqual(config.get("other::name"), "ATC自动化测试^-^&($#……#") + self.assertEqual(config.get("other::key1"), "test key") + + config.set("other::key1", "new test key") + + self.assertEqual(config.get("other::key1"), "new test key") + + config.set("other::key1", "test key") + + self.assertEqual(config.get("multi1::name"), "r.sub==p.sub&&r.obj==p.obj") + self.assertEqual(config.get("multi2::name"), "r.sub==p.sub&&r.obj==p.obj") + self.assertEqual(config.get("multi3::name"), "r.sub==p.sub&&r.obj==p.obj") + self.assertEqual(config.get("multi4::name"), "") + self.assertEqual(config.get("multi5::name"), "r.sub==p.sub&&r.obj==p.obj") diff --git a/tests/log/test_log.py b/tests/log/test_log.py new file mode 100644 index 00000000..aad7afb0 --- /dev/null +++ b/tests/log/test_log.py @@ -0,0 +1,12 @@ +from unittest import TestCase +from casbin import log + + +class TestLog(TestCase): + + def test_log(self): + log.get_logger().enable_log(True) + log.log_print("test log", "print") + log.log_printf("test log %s", "print") + + self.assertTrue(log.get_logger().is_enabled()) diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py new file mode 100644 index 00000000..7afc3403 --- /dev/null +++ b/tests/test_enforcer.py @@ -0,0 +1,123 @@ +import casbin +import os +from unittest import TestCase + + +def get_enforcer(model=None, adapter=None, enable_log=False): + return casbin.Enforcer( + model, + adapter, + enable_log, + ) + + +def get_examples(path): + examples_path = os.path.split(os.path.realpath(__file__))[0] + "/../examples/" + return os.path.abspath(examples_path + path) + + +class TestConfig(TestCase): + def test_enforcer_basic(self): + e = get_enforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + # True, + ) + + self.assertTrue(e.enforce('alice', 'data1', 'read')) + self.assertFalse(e.enforce('alice', 'data2', 'read')) + self.assertTrue(e.enforce('bob', 'data2', 'write')) + self.assertFalse(e.enforce('bob', 'data1', 'write')) + + def test_enforce_basic_with_root(self): + e = get_enforcer(get_examples("basic_with_root_model.conf"), get_examples("basic_policy.csv")) + self.assertTrue(e.enforce('root', 'any', 'any')) + + def test_enforce_basic_without_resources(self): + e = get_enforcer(get_examples("basic_without_resources_model.conf"), + get_examples("basic_without_resources_policy.csv")) + self.assertTrue(e.enforce('alice', 'read')) + self.assertFalse(e.enforce('alice', 'write')) + self.assertTrue(e.enforce('bob', 'write')) + self.assertFalse(e.enforce('bob', 'read')) + + def test_enforce_basic_without_users(self): + e = get_enforcer(get_examples("basic_without_users_model.conf"), + get_examples("basic_without_users_policy.csv")) + self.assertTrue(e.enforce('data1', 'read')) + self.assertFalse(e.enforce('data1', 'write')) + self.assertTrue(e.enforce('data2', 'write')) + self.assertFalse(e.enforce('data2', 'read')) + + def test_enforce_ip_match(self): + e = get_enforcer(get_examples("ipmatch_model.conf"), + get_examples("ipmatch_policy.csv")) + self.assertTrue(e.enforce('192.168.2.1', 'data1', 'read')) + self.assertFalse(e.enforce('192.168.3.1', 'data1', 'read')) + + def test_enforce_key_match(self): + e = get_enforcer(get_examples("keymatch_model.conf"), + get_examples("keymatch_policy.csv")) + self.assertTrue(e.enforce('alice', '/alice_data/test', 'GET')) + self.assertFalse(e.enforce('alice', '/bob_data/test', 'GET')) + self.assertTrue(e.enforce('cathy', '/cathy_data', 'GET')) + self.assertTrue(e.enforce('cathy', '/cathy_data', 'POST')) + self.assertFalse(e.enforce('cathy', '/cathy_data/12', 'POST')) + + def test_enforce_key_match2(self): + e = get_enforcer(get_examples("keymatch2_model.conf"), + get_examples("keymatch2_policy.csv")) + self.assertTrue(e.enforce('alice', '/alice_data/resource', 'GET')) + self.assertTrue(e.enforce('alice', '/alice_data2/123/using/456', 'GET')) + + def test_enforce_priority(self): + e = get_enforcer(get_examples("priority_model.conf"), get_examples("priority_policy.csv")) + 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.assertTrue(e.enforce('bob', 'data2', 'read')) + self.assertFalse(e.enforce('bob', 'data2', 'write')) + + def test_enforce_priority_indeterminate(self): + e = get_enforcer(get_examples("priority_model.conf"), get_examples("priority_indeterminate_policy.csv")) + self.assertFalse(e.enforce('alice', 'data1', 'read')) + + def test_enforce_rbac_with_deny(self): + e = get_enforcer(get_examples("rbac_with_deny_model.conf"), get_examples("rbac_with_deny_policy.csv")) + self.assertTrue(e.enforce('alice', 'data1', 'read')) + self.assertTrue(e.enforce('bob', 'data2', 'write')) + self.assertTrue(e.enforce('alice', 'data2', 'read')) + self.assertFalse(e.enforce('alice', 'data2', 'write')) + + def test_enforce_rbac_with_domains(self): + e = get_enforcer(get_examples("rbac_with_domains_model.conf"), get_examples("rbac_with_domains_policy.csv")) + self.assertTrue(e.enforce('alice', 'domain1', 'data1', 'read')) + self.assertTrue(e.enforce('alice', 'domain1', 'data1', 'write')) + self.assertFalse(e.enforce('alice', 'domain1', 'data2', 'read')) + self.assertFalse(e.enforce('alice', 'domain1', 'data2', 'write')) + + self.assertFalse(e.enforce('bob', 'domain2', 'data1', 'read')) + self.assertFalse(e.enforce('bob', 'domain2', 'data1', 'write')) + self.assertTrue(e.enforce('bob', 'domain2', 'data2', 'read')) + self.assertTrue(e.enforce('bob', 'domain2', 'data2', 'write')) + + def test_enforce_rbac_with_not_deny(self): + e = get_enforcer(get_examples("rbac_with_not_deny_model.conf"), get_examples("rbac_with_deny_policy.csv")) + self.assertFalse(e.enforce('alice', 'data2', 'write')) + + def test_enforce_rbac_with_resource_roles(self): + e = get_enforcer(get_examples("rbac_with_resource_roles_model.conf"), + get_examples("rbac_with_resource_roles_policy.csv")) + self.assertTrue(e.enforce('alice', 'data1', 'read')) + self.assertTrue(e.enforce('alice', 'data1', 'write')) + self.assertFalse(e.enforce('alice', 'data2', 'read')) + self.assertTrue(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/util/test_builtin_operators.py b/tests/util/test_builtin_operators.py new file mode 100644 index 00000000..c57e2ecc --- /dev/null +++ b/tests/util/test_builtin_operators.py @@ -0,0 +1,44 @@ +from unittest import TestCase +from casbin import util + + +class TestBuiltinOperators(TestCase): + + def test_key_match(self): + self.assertTrue(util.key_match_func("/foo", "/foo")) + self.assertTrue(util.key_match_func("/foo", "/foo*")) + self.assertFalse(util.key_match_func("/foo", "/foo/*")) + self.assertFalse(util.key_match_func("/foo/bar", "/foo")) + self.assertTrue(util.key_match_func("/foo/bar", "/foo*")) + self.assertTrue(util.key_match_func("/foo/bar", "/foo/*")) + self.assertFalse(util.key_match_func("/foobar", "/foo")) + self.assertTrue(util.key_match_func("/foobar", "/foo*")) + self.assertFalse(util.key_match_func("/foobar", "/foo/*")) + + def test_key_match2(self): + self.assertTrue(util.key_match2_func("/foo", "/foo")) + self.assertTrue(util.key_match2_func("/foo", "/foo*")) + self.assertFalse(util.key_match2_func("/foo", "/foo/*")) + self.assertTrue(util.key_match2_func("/foo/bar", "/foo")) # different with KeyMatch. + self.assertTrue(util.key_match2_func("/foo/bar", "/foo*")) + self.assertTrue(util.key_match2_func("/foo/bar", "/foo/*")) + self.assertTrue(util.key_match2_func("/foobar", "/foo")) # different with KeyMatch. + self.assertTrue(util.key_match2_func("/foobar", "/foo*")) + self.assertFalse(util.key_match2_func("/foobar", "/foo/*")) + + self.assertFalse(util.key_match2_func("/", "/:resource")) + self.assertTrue(util.key_match2_func("/resource1", "/:resource")) + self.assertFalse(util.key_match2_func("/myid", "/:id/using/:resId")) + self.assertTrue(util.key_match2_func("/myid/using/myresid", "/:id/using/:resId")) + + self.assertFalse(util.key_match2_func("/proxy/myid", "/proxy/:id/*")) + self.assertTrue(util.key_match2_func("/proxy/myid/", "/proxy/:id/*")) + self.assertTrue(util.key_match2_func("/proxy/myid/res", "/proxy/:id/*")) + self.assertTrue(util.key_match2_func("/proxy/myid/res/res2", "/proxy/:id/*")) + self.assertTrue(util.key_match2_func("/proxy/myid/res/res2/res3", "/proxy/:id/*")) + self.assertFalse(util.key_match2_func("/proxy/", "/proxy/:id/*")) + + self.assertTrue(util.key_match2_func("/alice", "/:id")) + self.assertTrue(util.key_match2_func("/alice/all", "/:id/all")) + self.assertFalse(util.key_match2_func("/alice", "/:id/all")) + self.assertFalse(util.key_match2_func("/alice/all", "/:id")) diff --git a/tests/util/test_util.py b/tests/util/test_util.py new file mode 100644 index 00000000..4f75ab09 --- /dev/null +++ b/tests/util/test_util.py @@ -0,0 +1,26 @@ +from unittest import TestCase +from casbin import util + + +class TestUtil(TestCase): + + def test_remove_comments(self): + self.assertEqual(util.remove_comments("r.act == p.act # comments"), "r.act == p.act") + self.assertEqual(util.remove_comments("r.act == p.act#comments"), "r.act == p.act") + self.assertEqual(util.remove_comments("r.act == p.act###"), "r.act == p.act") + self.assertEqual(util.remove_comments("### comments"), "") + self.assertEqual(util.remove_comments("r.act == p.act"), "r.act == p.act") + + def test_escape_assertion(self): + self.assertEqual(util.escape_assertion("m = r.sub == p.sub && r.obj == p.obj && r.act == p.act"), + "m = r_sub == p_sub && r_obj == p_obj && r_act == p_act") + + def test_array_remove_duplicates(self): + res = util.array_remove_duplicates(["data", "data1", "data2", "data1", "data2", "data3"]) + self.assertEqual(res, ['data', 'data1', 'data2', 'data3']) + + def test_array_to_string(self): + self.assertEqual(util.array_to_string(['data', 'data1', 'data2', 'data3']), "data, data1, data2, data3") + + def test_params_to_string(self): + self.assertEqual(util.params_to_string('data', 'data1', 'data2', 'data3'), "data, data1, data2, data3")