├── .coveragerc ├── .github ├── semantic.yml └── workflows │ └── build.yml ├── .gitignore ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── casbin_adapter ├── __init__.py ├── adapter.py ├── apps.py ├── enforcer.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py └── utils.py ├── package.json ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt └── tests ├── __init__.py ├── rbac_model.conf ├── rbac_policy.csv ├── settings.py └── test_adapter.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = casbin_adapter/* 3 | omit = *migrations*, *tests* -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Always validate the PR title AND all the commits 2 | titleAndCommits: true 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: ['3.10', '3.11', '3.12'] 15 | os: [ubuntu-latest, macOS-latest, windows-latest] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Install dependencies 27 | run: | 28 | pip install -r requirements_dev.txt 29 | pip install coveralls 30 | pip install pytest 31 | 32 | - name: Run tests 33 | run: coverage run -m django test --settings=tests.settings 34 | 35 | - name: Upload coverage data to coveralls.io 36 | run: coveralls --service=github 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }} 40 | COVERALLS_PARALLEL: true 41 | 42 | lint: 43 | name: Run Linters 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v2 48 | with: 49 | fetch-depth: 0 50 | 51 | - name: Super-Linter 52 | uses: github/super-linter@v4.9.2 53 | env: 54 | VALIDATE_ALL_CODEBASE: false 55 | VALIDATE_PYTHON_BLACK: true 56 | DEFAULT_BRANCH: master 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | LINTER_RULES_PATH: / 59 | PYTHON_BLACK_CONFIG_FILE: pyproject.toml 60 | 61 | coveralls: 62 | name: Indicate completion to coveralls.io 63 | needs: test 64 | runs-on: ubuntu-latest 65 | container: python:3-slim 66 | steps: 67 | - name: Finished 68 | run: | 69 | pip3 install --upgrade coveralls 70 | coveralls --finish 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | release: 75 | name: Release 76 | runs-on: ubuntu-latest 77 | needs: [ test, coveralls ] 78 | steps: 79 | - name: Checkout 80 | uses: actions/checkout@v2 81 | with: 82 | fetch-depth: 0 83 | 84 | - name: Setup Node.js 85 | uses: actions/setup-node@v2 86 | with: 87 | node-version: '20' 88 | 89 | - name: Setup 90 | run: npm install 91 | 92 | - name: Set up python 93 | uses: actions/setup-python@v4 94 | with: 95 | python-version: '3.12' 96 | 97 | - name: Release 98 | env: 99 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 100 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 101 | run: npx semantic-release 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | *.iml 133 | 134 | .DS_Store 135 | 136 | # node 137 | node_modules 138 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": "master", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "semantic-release-pypi", 7 | "@semantic-release/github", 8 | [ 9 | "@semantic-release/changelog", 10 | { 11 | "changelogFile": "CHANGELOG.md", 12 | "changelogTitle": "# Semantic Versioning Changelog" 13 | } 14 | ], 15 | [ 16 | "@semantic-release/git", 17 | { 18 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", 19 | "assets": ["CHANGELOG.md", "pyproject.toml"] 20 | } 21 | ] 22 | ] 23 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Semantic Versioning Changelog 2 | 3 | # [1.3.0](https://github.com/pycasbin/django-orm-adapter/compare/v1.2.0...v1.3.0) (2024-03-29) 4 | 5 | 6 | ### Features 7 | 8 | * upgrade CI Node.js version to 20 ([4beb0ba](https://github.com/pycasbin/django-orm-adapter/commit/4beb0baf43e55a3ba2fdb6c30b7110135d66f8ab)) 9 | 10 | # [1.2.0](https://github.com/pycasbin/django-orm-adapter/compare/v1.1.3...v1.2.0) (2023-11-03) 11 | 12 | 13 | ### Features 14 | 15 | * code refactoring and bug fixing ([#29](https://github.com/pycasbin/django-orm-adapter/issues/29)) ([81557ff](https://github.com/pycasbin/django-orm-adapter/commit/81557ffb7c7b2756fc2676662a693cd2d684e20e)) 16 | 17 | ## [1.1.3](https://github.com/pycasbin/django-orm-adapter/compare/v1.1.2...v1.1.3) (2023-10-28) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * migrate setup.py to pyproject.toml ([#27](https://github.com/pycasbin/django-orm-adapter/issues/27)) ([1c69f62](https://github.com/pycasbin/django-orm-adapter/commit/1c69f6220975e7a3996947cab6b0c3048ca8ed9a)) 23 | 24 | ## [1.1.2](https://github.com/pycasbin/django-orm-adapter/compare/v1.1.1...v1.1.2) (2023-08-08) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * fix broken links ([#25](https://github.com/pycasbin/django-orm-adapter/issues/25)) ([483325f](https://github.com/pycasbin/django-orm-adapter/commit/483325f885c59dd54548d097c7a592b0cde8f569)) 30 | 31 | ## [1.1.1](https://github.com/pycasbin/django-orm-adapter/compare/v1.1.0...v1.1.1) (2023-08-02) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * initialize with db_alias ([#24](https://github.com/pycasbin/django-orm-adapter/issues/24)) ([e3f283f](https://github.com/pycasbin/django-orm-adapter/commit/e3f283fd6e07efef037ab552f81bb9061a4fc563)) 37 | 38 | # [1.1.0](https://github.com/pycasbin/django-orm-adapter/compare/v1.0.3...v1.1.0) (2023-07-31) 39 | 40 | 41 | ### Features 42 | 43 | * add db alias ([#21](https://github.com/pycasbin/django-orm-adapter/issues/21)) ([a6d61da](https://github.com/pycasbin/django-orm-adapter/commit/a6d61da8943ab839e4452478abb15a25d68067a5)) 44 | 45 | ## [1.0.3](https://github.com/pycasbin/django-orm-adapter/compare/v1.0.2...v1.0.3) (2023-07-29) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * remove unused argument enable_log ([#20](https://github.com/pycasbin/django-orm-adapter/issues/20)) ([aa3e0a3](https://github.com/pycasbin/django-orm-adapter/commit/aa3e0a38132a212f6cbf4908cd2978b405a5964c)) 51 | 52 | ## [1.0.2](https://github.com/pycasbin/django-orm-adapter/compare/v1.0.1...v1.0.2) (2022-08-15) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * add error handler when initial django app ([356a0f3](https://github.com/pycasbin/django-orm-adapter/commit/356a0f3a42a5399488cc83eba9b3d0f1cec70e8f)) 58 | 59 | ## [1.0.1](https://github.com/pycasbin/django-orm-adapter/compare/v1.0.0...v1.0.1) (2022-08-02) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * compatible with new pycasbin ([#15](https://github.com/pycasbin/django-orm-adapter/issues/15)) ([9cea17d](https://github.com/pycasbin/django-orm-adapter/commit/9cea17d916e9b10177a626a6123bc9d21327083f)) 65 | 66 | # 1.0.0 (2022-07-30) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * lint ([7d3125f](https://github.com/pycasbin/django-orm-adapter/commit/7d3125fd76c01c0004e01311b9ba3e8af186c7a3)) 72 | * migrate from travis-ci to Github Actions ([1799eab](https://github.com/pycasbin/django-orm-adapter/commit/1799eab6aa105e59a2df1e301b5e08602f89b5fb)) 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django ORM Adapter for PyCasbin 2 | 3 | [![GitHub Action](https://github.com/pycasbin/django-orm-adapter/workflows/build/badge.svg?branch=master)](https://github.com/pycasbin/django-orm-adapter/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/pycasbin/django-orm-adapter/badge.svg)](https://coveralls.io/github/pycasbin/django-orm-adapter) 5 | [![Version](https://img.shields.io/pypi/v/casbin-django-orm-adapter.svg)](https://pypi.org/project/casbin-django-orm-adapter/) 6 | [![PyPI - Wheel](https://img.shields.io/pypi/wheel/casbin-django-orm-adapter.svg)](https://pypi.org/project/casbin-django-orm-adapter/) 7 | [![Pyversions](https://img.shields.io/pypi/pyversions/casbin-django-orm-adapter.svg)](https://pypi.org/project/casbin-django-orm-adapter/) 8 | [![Download](https://img.shields.io/pypi/dm/casbin-django-orm-adapter.svg)](https://pypi.org/project/casbin-django-orm-adapter/) 9 | [![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN) 10 | 11 | Django ORM Adapter is the [Django](https://www.djangoproject.com/)'s [ORM](https://docs.djangoproject.com/en/3.0/ref/databases/) adapter for [PyCasbin](https://github.com/pycasbin/django-orm-adapter). With this library, Casbin can load policy from Django ORM supported database or save policy to it. 12 | 13 | Based on [Officially Supported Databases](https://docs.djangoproject.com/en/3.0/ref/databases/), The current supported databases are: 14 | 15 | - PostgreSQL 16 | - MariaDB 17 | - MySQL 18 | - Oracle 19 | - SQLite 20 | - IBM DB2 21 | - Microsoft SQL Server 22 | - Firebird 23 | - ODBC 24 | 25 | ## Installation 26 | 27 | ``` 28 | pip install casbin-django-orm-adapter 29 | ``` 30 | 31 | Add `casbin_adapter.apps.CasbinAdapterConfig` to your `INSTALLED_APPS` 32 | 33 | ```python 34 | # settings.py 35 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 36 | 37 | INSTALLED_APPS = [ 38 | ... 39 | 'casbin_adapter.apps.CasbinAdapterConfig', 40 | ... 41 | ] 42 | 43 | CASBIN_MODEL = os.path.join(BASE_DIR, 'casbin.conf') 44 | ``` 45 | 46 | To run schema migration, execute `python manage.py migrate casbin_adapter` 47 | 48 | ## Simple Example 49 | 50 | ```python 51 | # views.py 52 | from casbin_adapter.enforcer import enforcer 53 | 54 | def hello(request): 55 | sub = "alice" # the user that wants to access a resource. 56 | obj = "data1" # the resource that is going to be accessed. 57 | act = "read" # the operation that the user performs on the resource. 58 | 59 | if e.enforce(sub, obj, act): 60 | # permit alice to read data1casbin_django_orm_adapter 61 | pass 62 | else: 63 | # deny the request, show an error 64 | pass 65 | ``` 66 | 67 | ## Configuration 68 | 69 | ### `CASBIN_MODEL` 70 | A string containing the file location of your casbin model. 71 | 72 | ### `CASBIN_ADAPTER` 73 | A string containing the adapter import path. Default to the django adapter shipped with this package: `casbin_adapter.adapter.Adapter` 74 | 75 | ### `CASBIN_ADAPTER_ARGS` 76 | A tuple of arguments to be passed into the constructor of the adapter specified 77 | in `CASBIN_ADAPTER`. Refer to adapters to see available arguments. 78 | 79 | E.g. if you wish to use the file adapter 80 | set the adapter to `casbin.persist.adapters.FileAdapter` and use 81 | `CASBIN_ADAPTER_ARGS = ('path/to/policy_file.csv',)` 82 | 83 | ### `CASBIN_DB_ALIAS` 84 | The database the adapter uses. Default to "default". 85 | 86 | ### `CASBIN_WATCHER` 87 | Watcher instance to be set as the watcher on the enforcer instance. 88 | 89 | ### `CASBIN_ROLE_MANAGER` 90 | Role manager instance to be set as the role manager on the enforcer instance. 91 | 92 | 93 | ### Getting Help 94 | 95 | - [PyCasbin](https://github.com/casbin/pycasbin) 96 | 97 | ### License 98 | 99 | This project is licensed under the [Apache 2.0 license](LICENSE). 100 | -------------------------------------------------------------------------------- /casbin_adapter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycasbin/django-orm-adapter/0a7c11ac227cad4b65febd60862ea42e4af9c11c/casbin_adapter/__init__.py -------------------------------------------------------------------------------- /casbin_adapter/adapter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from casbin import persist 4 | from django.db.utils import OperationalError, ProgrammingError 5 | 6 | from .models import CasbinRule 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Adapter(persist.Adapter): 12 | """the interface for Casbin adapters.""" 13 | 14 | def __init__(self, db_alias="default"): 15 | self.db_alias = db_alias 16 | 17 | def load_policy(self, model): 18 | """loads all policy rules from the storage.""" 19 | try: 20 | lines = CasbinRule.objects.using(self.db_alias).all() 21 | 22 | for line in lines: 23 | persist.load_policy_line(str(line), model) 24 | except (OperationalError, ProgrammingError) as error: 25 | logger.warning("Could not load policy from database: {}".format(error)) 26 | 27 | def _create_policy_line(self, ptype, rule): 28 | line = CasbinRule(ptype=ptype) 29 | if len(rule) > 0: 30 | line.v0 = rule[0] 31 | if len(rule) > 1: 32 | line.v1 = rule[1] 33 | if len(rule) > 2: 34 | line.v2 = rule[2] 35 | if len(rule) > 3: 36 | line.v3 = rule[3] 37 | if len(rule) > 4: 38 | line.v4 = rule[4] 39 | if len(rule) > 5: 40 | line.v5 = rule[5] 41 | return line 42 | 43 | def save_policy(self, model): 44 | """saves all policy rules to the storage.""" 45 | # See https://casbin.org/docs/adapters/#autosave 46 | # for why this is deleting all rules 47 | CasbinRule.objects.using(self.db_alias).all().delete() 48 | 49 | lines = [] 50 | for sec in ["p", "g"]: 51 | if sec not in model.model.keys(): 52 | continue 53 | for ptype, ast in model.model[sec].items(): 54 | for rule in ast.policy: 55 | lines.append(self._create_policy_line(ptype, rule)) 56 | rows_created = CasbinRule.objects.using(self.db_alias).bulk_create(lines) 57 | return len(rows_created) > 0 58 | 59 | def add_policy(self, sec, ptype, rule): 60 | """adds a policy rule to the storage.""" 61 | line = self._create_policy_line(ptype, rule) 62 | line.save() 63 | 64 | def remove_policy(self, sec, ptype, rule): 65 | """removes a policy rule from the storage.""" 66 | query_params = {"ptype": ptype} 67 | for i, v in enumerate(rule): 68 | query_params["v{}".format(i)] = v 69 | rows_deleted, _ = CasbinRule.objects.using(self.db_alias).filter(**query_params).delete() 70 | return rows_deleted > 0 71 | 72 | def remove_filtered_policy(self, sec, ptype, field_index, *field_values): 73 | """removes policy rules that match the filter from the storage. 74 | This is part of the Auto-Save feature. 75 | """ 76 | query_params = {"ptype": ptype} 77 | if not (0 <= field_index <= 5): 78 | return False 79 | if not (1 <= field_index + len(field_values) <= 6): 80 | return False 81 | for i, v in enumerate(field_values): 82 | query_params["v{}".format(i + field_index)] = v 83 | rows_deleted, _ = CasbinRule.objects.using(self.db_alias).filter(**query_params).delete() 84 | return rows_deleted > 0 85 | -------------------------------------------------------------------------------- /casbin_adapter/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.conf import settings 3 | 4 | 5 | class CasbinAdapterConfig(AppConfig): 6 | name = "casbin_adapter" 7 | 8 | def ready(self): 9 | from .enforcer import initialize_enforcer 10 | 11 | db_alias = getattr(settings, "CASBIN_DB_ALIAS", "default") 12 | initialize_enforcer(db_alias) 13 | -------------------------------------------------------------------------------- /casbin_adapter/enforcer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.conf import settings 3 | from django.db import connection, connections 4 | from django.db.utils import OperationalError, ProgrammingError 5 | 6 | from casbin import Enforcer 7 | 8 | from .utils import import_class 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class ProxyEnforcer(Enforcer): 14 | _initialized = False 15 | db_alias = "default" 16 | 17 | def __init__(self, *args, **kwargs): 18 | if self._initialized: 19 | super().__init__(*args, **kwargs) 20 | else: 21 | logger.info("Deferring casbin enforcer initialisation until django is ready") 22 | 23 | def _load(self): 24 | if self._initialized is False: 25 | logger.info("Performing deferred casbin enforcer initialisation") 26 | self._initialized = True 27 | model = getattr(settings, "CASBIN_MODEL") 28 | adapter_loc = getattr(settings, "CASBIN_ADAPTER", "casbin_adapter.adapter.Adapter") 29 | adapter_args = getattr(settings, "CASBIN_ADAPTER_ARGS", tuple()) 30 | self.db_alias = getattr(settings, "CASBIN_DB_ALIAS", "default") 31 | Adapter = import_class(adapter_loc) 32 | adapter = Adapter(self.db_alias, *adapter_args) 33 | 34 | super().__init__(model, adapter) 35 | logger.debug("Casbin enforcer initialised") 36 | 37 | watcher = getattr(settings, "CASBIN_WATCHER", None) 38 | if watcher: 39 | self.set_watcher(watcher) 40 | 41 | role_manager = getattr(settings, "CASBIN_ROLE_MANAGER", None) 42 | if role_manager: 43 | self.set_role_manager(role_manager) 44 | 45 | def __getattribute__(self, name): 46 | safe_methods = ["__init__", "_load", "_initialized"] 47 | if not super().__getattribute__("_initialized") and name not in safe_methods: 48 | initialize_enforcer(self.db_alias) 49 | if not super().__getattribute__("_initialized"): 50 | raise Exception( 51 | ( 52 | "Calling enforcer attributes before django registry is ready. " 53 | "Prevent making any calls to the enforcer on import/startup" 54 | ) 55 | ) 56 | 57 | return super().__getattribute__(name) 58 | 59 | 60 | enforcer = ProxyEnforcer() 61 | 62 | 63 | def initialize_enforcer(db_alias=None): 64 | try: 65 | row = None 66 | connect = connections[db_alias] if db_alias else connection 67 | with connect.cursor() as cursor: 68 | cursor.execute( 69 | """ 70 | SELECT app, name applied FROM django_migrations 71 | WHERE app = 'casbin_adapter' AND name = '0001_initial'; 72 | """ 73 | ) 74 | row = cursor.fetchone() 75 | 76 | if row: 77 | enforcer._load() 78 | except (OperationalError, ProgrammingError): 79 | pass 80 | -------------------------------------------------------------------------------- /casbin_adapter/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-03-02 15:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="CasbinRule", 15 | fields=[ 16 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 17 | ("ptype", models.CharField(max_length=255)), 18 | ("v0", models.CharField(max_length=255)), 19 | ("v1", models.CharField(max_length=255)), 20 | ("v2", models.CharField(max_length=255)), 21 | ("v3", models.CharField(max_length=255)), 22 | ("v4", models.CharField(max_length=255)), 23 | ("v5", models.CharField(max_length=255)), 24 | ], 25 | options={ 26 | "db_table": "casbin_rule", 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /casbin_adapter/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycasbin/django-orm-adapter/0a7c11ac227cad4b65febd60862ea42e4af9c11c/casbin_adapter/migrations/__init__.py -------------------------------------------------------------------------------- /casbin_adapter/models.py: -------------------------------------------------------------------------------- 1 | # Copied from: https://github.com/pycasbin/django-orm-adapter 2 | # as it lacks a setup.py file atm 3 | from django.db import models 4 | 5 | 6 | class CasbinRule(models.Model): 7 | ptype = models.CharField(max_length=255) 8 | v0 = models.CharField(max_length=255) 9 | v1 = models.CharField(max_length=255) 10 | v2 = models.CharField(max_length=255) 11 | v3 = models.CharField(max_length=255) 12 | v4 = models.CharField(max_length=255) 13 | v5 = models.CharField(max_length=255) 14 | 15 | class Meta: 16 | db_table = "casbin_rule" 17 | 18 | def __str__(self): 19 | text = self.ptype 20 | 21 | if self.v0: 22 | text = text + ", " + self.v0 23 | if self.v1: 24 | text = text + ", " + self.v1 25 | if self.v2: 26 | text = text + ", " + self.v2 27 | if self.v3: 28 | text = text + ", " + self.v3 29 | if self.v4: 30 | text = text + ", " + self.v4 31 | if self.v5: 32 | text = text + ", " + self.v5 33 | return text 34 | 35 | def __repr__(self): 36 | return ''.format(self.id, str(self)) 37 | -------------------------------------------------------------------------------- /casbin_adapter/utils.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | 4 | def import_class(name): 5 | """Import class from string 6 | e.g. `package.module.ClassToImport` returns the `ClassToImport` class""" 7 | components = name.split(".") 8 | module_name = ".".join(components[:-1]) 9 | class_name = components[-1] 10 | module = importlib.import_module(module_name) 11 | return getattr(module, class_name) 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@semantic-release/changelog": "^6.0.3", 4 | "@semantic-release/git": "^10.0.1", 5 | "semantic-release": "^22.0.5", 6 | "semantic-release-pypi": "^3.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "casbin-django-orm-adapter" 3 | version = "1.3.0" 4 | authors = [ 5 | {name = "Yang Luo", email = "hsluoyz@qq.com"}, 6 | ] 7 | description = "Django's ORM adapter for PyCasbin" 8 | readme = "README.md" 9 | keywords = [ 10 | "casbin", 11 | "adapter", 12 | "storage-driver", 13 | "django", 14 | "orm", 15 | "django-orm", 16 | "access-control", 17 | "authorization" 18 | ] 19 | dynamic = ["dependencies"] 20 | requires-python = ">=3.7" 21 | license = {text = "Apache 2.0"} 22 | classifiers = [ 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "License :: OSI Approved :: Apache Software License", 28 | "Operating System :: OS Independent", 29 | ] 30 | 31 | [project.urls] 32 | Home-page = "https://github.com/pycasbin/django-orm-adapter" 33 | 34 | [build-system] 35 | requires = ["setuptools"] 36 | build-backend = "setuptools.build_meta" 37 | 38 | [tool.setuptools.packages.find] 39 | exclude = ["tests"] 40 | 41 | [tool.setuptools.dynamic] 42 | dependencies = {file = ["requirements.txt"]} 43 | 44 | [tool.black] 45 | line-length = 120 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | casbin>=1.16.10 2 | Django 3 | 4 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | setuptools==70.0.0 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycasbin/django-orm-adapter/0a7c11ac227cad4b65febd60862ea42e4af9c11c/tests/__init__.py -------------------------------------------------------------------------------- /tests/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 15 | -------------------------------------------------------------------------------- /tests/rbac_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, data1, read 2 | p, bob, data2, write 3 | p, data2_admin, data2, read 4 | p, data2_admin, data2, write 5 | 6 | g, alice, data2_admin 7 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = "not-a-production-secret" 6 | 7 | INSTALLED_APPS = [ 8 | "django.contrib.admin", 9 | "django.contrib.auth", 10 | "django.contrib.contenttypes", 11 | "django.contrib.sessions", 12 | "django.contrib.messages", 13 | "django.contrib.staticfiles", 14 | "casbin_adapter.apps.CasbinAdapterConfig", 15 | "tests", 16 | ] 17 | 18 | MIDDLEWARE = [ 19 | "django.middleware.security.SecurityMiddleware", 20 | "django.contrib.sessions.middleware.SessionMiddleware", 21 | "django.middleware.common.CommonMiddleware", 22 | "django.middleware.csrf.CsrfViewMiddleware", 23 | "django.contrib.auth.middleware.AuthenticationMiddleware", 24 | "django.contrib.messages.middleware.MessageMiddleware", 25 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 26 | ] 27 | 28 | TEMPLATES = [ 29 | { 30 | "BACKEND": "django.template.backends.django.DjangoTemplates", 31 | "APP_DIRS": True, 32 | "OPTIONS": { 33 | "context_processors": [ 34 | "django.template.context_processors.debug", 35 | "django.template.context_processors.request", 36 | "django.contrib.auth.context_processors.auth", 37 | "django.contrib.messages.context_processors.messages", 38 | ] 39 | }, 40 | } 41 | ] 42 | 43 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 44 | 45 | CASBIN_MODEL = os.path.join(BASE_DIR, "tests", "rbac_model.conf") 46 | -------------------------------------------------------------------------------- /tests/test_adapter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import casbin 3 | import simpleeval 4 | 5 | from django.test import TestCase 6 | from casbin_adapter.models import CasbinRule 7 | from casbin_adapter.adapter import Adapter 8 | 9 | 10 | def get_fixture(path): 11 | dir_path = os.path.split(os.path.realpath(__file__))[0] + "/" 12 | return os.path.abspath(dir_path + path) 13 | 14 | 15 | def get_enforcer(): 16 | adapter = Adapter() 17 | 18 | CasbinRule.objects.bulk_create( 19 | [ 20 | CasbinRule(ptype="p", v0="alice", v1="data1", v2="read"), 21 | CasbinRule(ptype="p", v0="bob", v1="data2", v2="write"), 22 | CasbinRule(ptype="p", v0="data2_admin", v1="data2", v2="read"), 23 | CasbinRule(ptype="p", v0="data2_admin", v1="data2", v2="write"), 24 | CasbinRule(ptype="g", v0="alice", v1="data2_admin"), 25 | ] 26 | ) 27 | 28 | return casbin.Enforcer(get_fixture("rbac_model.conf"), adapter) 29 | 30 | 31 | class TestConfig(TestCase): 32 | def test_enforcer_basic(self): 33 | e = get_enforcer() 34 | self.assertTrue(e.enforce("alice", "data1", "read")) 35 | self.assertFalse(e.enforce("bob", "data1", "read")) 36 | self.assertTrue(e.enforce("bob", "data2", "write")) 37 | self.assertTrue(e.enforce("alice", "data2", "read")) 38 | self.assertTrue(e.enforce("alice", "data2", "write")) 39 | 40 | def test_add_policy(self): 41 | adapter = Adapter() 42 | e = casbin.Enforcer(get_fixture("rbac_model.conf"), adapter) 43 | 44 | try: 45 | self.assertFalse(e.enforce("alice", "data1", "read")) 46 | self.assertFalse(e.enforce("bob", "data1", "read")) 47 | self.assertFalse(e.enforce("bob", "data2", "write")) 48 | self.assertFalse(e.enforce("alice", "data2", "read")) 49 | self.assertFalse(e.enforce("alice", "data2", "write")) 50 | except simpleeval.NameNotDefined: 51 | # This is caused by an upstream bug when there is no policy loaded 52 | # Should be resolved in pycasbin >= 0.3 53 | pass 54 | 55 | adapter.add_policy(sec=None, ptype="p", rule=["alice", "data1", "read"]) 56 | adapter.add_policy(sec=None, ptype="p", rule=["bob", "data2", "write"]) 57 | adapter.add_policy(sec=None, ptype="p", rule=["data2_admin", "data2", "read"]) 58 | adapter.add_policy(sec=None, ptype="p", rule=["data2_admin", "data2", "write"]) 59 | adapter.add_policy(sec=None, ptype="g", rule=["alice", "data2_admin"]) 60 | 61 | e.load_policy() 62 | 63 | self.assertTrue(e.enforce("alice", "data1", "read")) 64 | self.assertFalse(e.enforce("bob", "data1", "read")) 65 | self.assertTrue(e.enforce("bob", "data2", "write")) 66 | self.assertTrue(e.enforce("alice", "data2", "read")) 67 | self.assertTrue(e.enforce("alice", "data2", "write")) 68 | self.assertFalse(e.enforce("bogus", "data2", "write")) 69 | 70 | def test_save_policy(self): 71 | model = casbin.Enforcer(get_fixture("rbac_model.conf"), get_fixture("rbac_policy.csv")).model 72 | adapter = Adapter() 73 | adapter.save_policy(model) 74 | e = casbin.Enforcer(get_fixture("rbac_model.conf"), adapter) 75 | 76 | self.assertTrue(e.enforce("alice", "data1", "read")) 77 | self.assertFalse(e.enforce("bob", "data1", "read")) 78 | self.assertTrue(e.enforce("bob", "data2", "write")) 79 | self.assertTrue(e.enforce("alice", "data2", "read")) 80 | self.assertTrue(e.enforce("alice", "data2", "write")) 81 | 82 | def test_autosave_off_doesnt_persist_to_db(self): 83 | adapter = Adapter() 84 | e = casbin.Enforcer(get_fixture("rbac_model.conf"), adapter) 85 | 86 | e.enable_auto_save(False) 87 | e.add_policy("alice", "data1", "write") 88 | e.load_policy() 89 | policies = e.get_policy() 90 | self.assertListEqual(policies, []) 91 | 92 | def test_autosave_on_persists_to_db(self): 93 | adapter = Adapter() 94 | e = casbin.Enforcer(get_fixture("rbac_model.conf"), adapter) 95 | 96 | e.enable_auto_save(True) 97 | e.add_policy("alice", "data1", "write") 98 | e.load_policy() 99 | policies = e.get_policy() 100 | self.assertListEqual(policies, [["alice", "data1", "write"]]) 101 | 102 | def test_autosave_on_persists_remove_action_to_db(self): 103 | adapter = Adapter() 104 | e = casbin.Enforcer(get_fixture("rbac_model.conf"), adapter) 105 | e.add_policy("alice", "data1", "write") 106 | e.load_policy() 107 | self.assertListEqual(e.get_policy(), [["alice", "data1", "write"]]) 108 | 109 | e.enable_auto_save(True) 110 | e.remove_policy("alice", "data1", "write") 111 | e.load_policy() 112 | self.assertListEqual(e.get_policy(), []) 113 | 114 | def test_remove_filtered_policy(self): 115 | e = get_enforcer() 116 | 117 | e.remove_filtered_policy(0, "data2_admin") 118 | e.load_policy() 119 | self.assertListEqual(e.get_policy(), [["alice", "data1", "read"], ["bob", "data2", "write"]]) 120 | 121 | def test_str(self): 122 | rule = CasbinRule(ptype="p", v0="alice", v1="data1", v2="read") 123 | self.assertEqual(str(rule), "p, alice, data1, read") 124 | 125 | def test_repr(self): 126 | rule = CasbinRule(ptype="p", v0="alice", v1="data1", v2="read") 127 | self.assertEqual(repr(rule), '') 128 | rule.save() 129 | self.assertRegex(repr(rule), r'') 130 | --------------------------------------------------------------------------------