├── .eslintrc.js ├── .github ├── semantic.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── examples ├── acl_model.conf ├── acl_policy.csv ├── rbac_model.conf └── rbac_policy.csv ├── package.json ├── src ├── adapter.ts ├── casbinMongoRule.ts ├── casbinRule.ts └── index.ts ├── test ├── adapter-config.test.ts ├── adapter-with-acl-model.test.ts ├── adapter-with-rbac-model.test.ts ├── config.ts └── existent-connection-adapter.test.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | ], 9 | env: { 10 | node: true, 11 | }, 12 | rules: { 13 | 'max-classes-per-file': 'off', 14 | '@typescript-eslint/no-empty-interface': 'off', 15 | 'no-console': 'off', 16 | 'arrow-parens': 'off', 17 | 'ordered-imports': 'off', 18 | 'object-literal-sort-keys': 'off', 19 | 'no-empty': 'off', 20 | 'quotes': ['error', 'single'], 21 | 'comma-dangle': 'off', 22 | 'max-len': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | '@typescript-eslint/ban-ts-comment': 'off', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Always validate the PR title AND all the commits 2 | titleAndCommits: true 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Set up Node.js 12 | uses: actions/setup-node@v2.1.5 13 | 14 | - name: Install Dependency 15 | run: yarn install 16 | 17 | - name: Run Lint 18 | run: yarn run lint 19 | 20 | - name: Run Fix 21 | run: yarn run fix 22 | 23 | coverage: 24 | needs: [lint] 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | 29 | - name: Setup MySQL 30 | uses: mirromutth/mysql-action@v1.1 31 | with: 32 | mysql database: casbin 33 | mysql root password: password 34 | 35 | - name: Set up Node.js 36 | uses: actions/setup-node@v2.1.5 37 | with: 38 | node-version: ^20 39 | 40 | - name: Install Dependency 41 | run: yarn install 42 | 43 | - name: Run Coverage 44 | run: yarn run coverage 45 | 46 | - name: Coveralls 47 | uses: coverallsapp/github-action@master 48 | with: 49 | github-token: ${{ secrets.GITHUB_TOKEN }} 50 | parallel: true 51 | 52 | test: 53 | needs: [lint] 54 | runs-on: ubuntu-latest 55 | strategy: 56 | matrix: 57 | node: [^18, ^20] 58 | steps: 59 | - uses: actions/checkout@v2 60 | 61 | - name: Setup MySQL 62 | uses: mirromutth/mysql-action@v1.1 63 | with: 64 | mysql database: casbin 65 | mysql root password: password 66 | 67 | - name: Set up Node.js 68 | uses: actions/setup-node@v3 69 | with: 70 | node-version: ${{ matrix.node }} 71 | 72 | - name: Install Dependency 73 | run: yarn install 74 | 75 | - name: Run Unit test 76 | run: yarn run test 77 | 78 | semantic-release: 79 | needs: [lint, test, coverage] 80 | runs-on: ubuntu-latest 81 | steps: 82 | - uses: actions/checkout@v2 83 | 84 | - name: Set up Node.js 85 | uses: actions/setup-node@v3 86 | with: 87 | node-version: ^20 88 | 89 | - name: Run semantic-release 90 | if: github.repository == 'node-casbin/typeorm-adapter' && github.event_name == 'push' 91 | run: | 92 | yarn install 93 | yarn run prepublish 94 | yarn run release 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 97 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules 4 | lib 5 | yarn-error.log 6 | package-lock.json 7 | coverage 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /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 | TypeORM Adapter 2 | ==== 3 | [![NPM version][npm-image]][npm-url] 4 | [![NPM download][download-image]][download-url] 5 | [![codebeat badge](https://codebeat.co/badges/7b938f17-ac89-4ee9-b3cc-787b5e94720d)](https://codebeat.co/projects/github-com-node-casbin-typeorm-adapter-master) 6 | [![CI](https://github.com/node-casbin/typeorm-adapter/actions/workflows/ci.yml/badge.svg)](https://github.com/node-casbin/typeorm-adapter/actions/workflows/ci.yml) 7 | [![Coverage Status](https://coveralls.io/repos/github/node-casbin/typeorm-adapter/badge.svg?branch=master)](https://coveralls.io/github/node-casbin/typeorm-adapter?branch=master) 8 | [![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN) 9 | 10 | [npm-image]: https://img.shields.io/npm/v/typeorm-adapter.svg?style=flat-square 11 | [npm-url]: https://npmjs.com/package/typeorm-adapter 12 | [download-image]: https://img.shields.io/npm/dm/typeorm-adapter.svg?style=flat-square 13 | [download-url]: https://npmjs.com/package/typeorm-adapter 14 | 15 | TypeORM Adapter is the [TypeORM](https://github.com/typeorm/typeorm) adapter for [Node-Casbin](https://github.com/casbin/node-casbin). With this library, Node-Casbin can load policy from TypeORM supported database or save policy to it. 16 | 17 | Based on [Officially Supported Databases](http://typeorm.io), the current supported databases are: 18 | 19 | - MySQL 20 | - PostgreSQL 21 | - MariaDB 22 | - SQLite 23 | - MS SQL Server 24 | - Oracle 25 | - WebSQL 26 | - MongoDB 27 | 28 | 29 | You may find other 3rd-party supported DBs in TypeORM website or other places. 30 | 31 | ## Installation 32 | 33 | npm install typeorm-adapter 34 | 35 | ## Simple Example 36 | 37 | ```typescript 38 | import { newEnforcer } from 'casbin'; 39 | import TypeORMAdapter from 'typeorm-adapter'; 40 | 41 | async function myFunction() { 42 | // Initialize a TypeORM adapter and use it in a Node-Casbin enforcer: 43 | // The adapter can not automatically create database. 44 | // But the adapter will automatically create and use the table named "casbin_rule". 45 | // I think ORM should not automatically create databases. 46 | const a = await TypeORMAdapter.newAdapter({ 47 | type: 'mysql', 48 | host: 'localhost', 49 | port: 3306, 50 | username: 'root', 51 | password: '', 52 | database: 'casbin', 53 | }); 54 | 55 | 56 | const e = await newEnforcer('examples/rbac_model.conf', a); 57 | 58 | // Load the policy from DB. 59 | await e.loadPolicy(); 60 | 61 | // Check the permission. 62 | await e.enforce('alice', 'data1', 'read'); 63 | 64 | // Modify the policy. 65 | // await e.addPolicy(...); 66 | // await e.removePolicy(...); 67 | 68 | // Save the policy back to DB. 69 | await e.savePolicy(); 70 | } 71 | ``` 72 | 73 | ## Simple Filter Example 74 | 75 | ```typescript 76 | import { newEnforcer } from 'casbin'; 77 | import TypeORMAdapter from 'typeorm-adapter'; 78 | 79 | async function myFunction() { 80 | // Initialize a TypeORM adapter and use it in a Node-Casbin enforcer: 81 | // The adapter can not automatically create database. 82 | // But the adapter will automatically create and use the table named "casbin_rule". 83 | // I think ORM should not automatically create databases. 84 | const a = await TypeORMAdapter.newAdapter({ 85 | type: 'mysql', 86 | host: 'localhost', 87 | port: 3306, 88 | username: 'root', 89 | password: '', 90 | database: 'casbin', 91 | }); 92 | 93 | 94 | const e = await newEnforcer('examples/rbac_model.conf', a); 95 | 96 | // Load the filtered policy from DB. 97 | await e.loadFilteredPolicy({ 98 | 'ptype': 'p', 99 | 'v0': 'alice' 100 | }); 101 | 102 | // Check the permission. 103 | await e.enforce('alice', 'data1', 'read'); 104 | 105 | // Modify the policy. 106 | // await e.addPolicy(...); 107 | // await e.removePolicy(...); 108 | 109 | // Save the policy back to DB. 110 | await e.savePolicy(); 111 | } 112 | ``` 113 | 114 | ## Custom Entity Example 115 | Use a custom entity that matches the CasbinRule or MongoCasbinRule in order to add additional fields or metadata to the entity. 116 | 117 | ```typescript 118 | import { newEnforcer } from 'casbin'; 119 | import { 120 | CreateDateColumn, 121 | UpdateDateColumn, 122 | } from 'typeorm'; 123 | import TypeORMAdapter from 'typeorm-adapter'; 124 | 125 | @Entity('custom_rule') 126 | class CustomCasbinRule extends CasbinRule { 127 | @CreateDateColumn() 128 | createdDate: Date; 129 | 130 | @UpdateDateColumn() 131 | updatedDate: Date; 132 | } 133 | 134 | async function myFunction() { 135 | // Initialize a TypeORM adapter and use it in a Node-Casbin enforcer: 136 | // The adapter can not automatically create database. 137 | // But the adapter will automatically create and use the table named "casbin_rule". 138 | // I think ORM should not automatically create databases. 139 | const a = await TypeORMAdapter.newAdapter({ 140 | type: 'mysql', 141 | host: 'localhost', 142 | port: 3306, 143 | username: 'root', 144 | password: '', 145 | database: 'casbin', 146 | }, 147 | { 148 | customCasbinRuleEntity: CustomCasbinRule, 149 | }, 150 | ); 151 | 152 | const e = await newEnforcer('examples/rbac_model.conf', a); 153 | 154 | // Load the filtered policy from DB. 155 | await e.loadFilteredPolicy({ 156 | 'ptype': 'p', 157 | 'v0': 'alice' 158 | }); 159 | 160 | // Check the permission. 161 | await e.enforce('alice', 'data1', 'read'); 162 | 163 | // Modify the policy. 164 | // await e.addPolicy(...); 165 | // await e.removePolicy(...); 166 | 167 | // Save the policy back to DB. 168 | await e.savePolicy(); 169 | } 170 | ``` 171 | ## Custom Database Table Name Example 172 | If you want to use a custom table name for the casbin rules, you need to: 173 | Create a custom entity class that inherits from CasbinRule and uses the @Entity decorator with your table name. 174 | Pass the custom entity class to the entities array of the data source constructor. 175 | Pass the custom entity class to the customCasbinRuleEntity option of the typeorm-adapter constructor. 176 | 177 | ```typescript 178 | import { newEnforcer } from 'casbin'; 179 | import { 180 | CreateDateColumn, 181 | UpdateDateColumn, 182 | } from 'typeorm'; 183 | import TypeORMAdapter from 'typeorm-adapter'; 184 | 185 | @Entity('custom_rule') 186 | class CustomCasbinRule extends CasbinRule { 187 | @CreateDateColumn() 188 | createdDate: Date; 189 | 190 | @UpdateDateColumn() 191 | updatedDate: Date; 192 | } 193 | 194 | async function myFunction() { 195 | // Initialize a TypeORM adapter and use it in a Node-Casbin enforcer: 196 | // The adapter can not automatically create database. 197 | // But the adapter will automatically create and use the table named "casbin_rule". 198 | // I think ORM should not automatically create databases. 199 | 200 | const datasource = new DataSource({ 201 | type: 'mysql', 202 | host: 'localhost', 203 | port: 3306, 204 | username: 'root', 205 | password: '', 206 | database: 'casbin', 207 | entities: [CustomCasbinRule], 208 | synchronize: true, 209 | }); 210 | 211 | const a = await TypeORMAdapter.newAdapter( 212 | { connection: datasource }, 213 | { 214 | customCasbinRuleEntity: CustomCasbinRule, 215 | }, 216 | ); 217 | 218 | const e = await newEnforcer('examples/rbac_model.conf', a); 219 | 220 | // Load the filtered policy from DB. 221 | await e.loadFilteredPolicy({ 222 | 'ptype': 'p', 223 | 'v0': 'alice' 224 | }); 225 | 226 | // Check the permission. 227 | await e.enforce('alice', 'data1', 'read'); 228 | 229 | // Modify the policy. 230 | // await e.addPolicy(...); 231 | // await e.removePolicy(...); 232 | 233 | // Save the policy back to DB. 234 | await e.savePolicy(); 235 | } 236 | ``` 237 | 238 | ## Getting Help 239 | 240 | - [Node-Casbin](https://github.com/casbin/node-casbin) 241 | 242 | ## License 243 | 244 | This project is under Apache 2.0 License. See the [LICENSE](LICENSE) file for the full license text. 245 | -------------------------------------------------------------------------------- /examples/acl_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [policy_effect] 8 | e = some(where (p.eft == allow)) 9 | 10 | [matchers] 11 | m = r.sub == p.sub && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /examples/acl_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, data1, read 2 | p, bob, data2, write 3 | -------------------------------------------------------------------------------- /examples/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 -------------------------------------------------------------------------------- /examples/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 | g, alice, data2_admin -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeorm-adapter", 3 | "version": "1.3.0", 4 | "description": "TypeORM adapter for Casbin", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "scripts": { 8 | "precommit": "lint-staged", 9 | "prepublish": "yarn run lint && yarn build", 10 | "build": "rimraf lib && tsc", 11 | "coverage": "jest --coverage --runInBand", 12 | "lint": "eslint \"src/**/*.{ts,js,jsx}\"", 13 | "fix": "eslint \"src/**/*.{ts,js,jsx}\" --fix", 14 | "test": "jest --runInBand", 15 | "release": "npx -p semantic-release -p @semantic-release/git -p @semantic-release/changelog semantic-release", 16 | "prepare": "npm run build" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^23.3.5", 20 | "@types/node": "^20.6.0", 21 | "@typescript-eslint/eslint-plugin": "^6.7.0", 22 | "@typescript-eslint/parser": "^6.7.0", 23 | "coveralls": "^3.0.2", 24 | "eslint": "^8.49.0", 25 | "husky": "^1.1.2", 26 | "jest": "^28.1.3", 27 | "lint-staged": "^7.3.0", 28 | "mysql2": "^2.1.0", 29 | "pg": "^8.4.2", 30 | "rimraf": "^2.6.2", 31 | "sqlite3": "^5.1.7", 32 | "ts-jest": "28.0.7", 33 | "typescript": "^5.2.2" 34 | }, 35 | "dependencies": { 36 | "casbin": "^5.27.0", 37 | "reflect-metadata": "^0.1.13", 38 | "typeorm": "^0.3.17" 39 | }, 40 | "files": [ 41 | "lib", 42 | "examples" 43 | ], 44 | "homepage": "https://casbin.org", 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/node-casbin/typeorm-adapter.git" 48 | }, 49 | "keywords": [ 50 | "casbin", 51 | "node-casbin", 52 | "adapter", 53 | "typeorm", 54 | "access-control", 55 | "authorization", 56 | "auth", 57 | "authz", 58 | "acl", 59 | "rbac", 60 | "abac", 61 | "orm" 62 | ], 63 | "author": "Node-Casbin", 64 | "licenses": [ 65 | { 66 | "type": "Apache-2.0", 67 | "url": "http://www.apache.org/licenses/LICENSE-2.0" 68 | } 69 | ], 70 | "bugs": { 71 | "url": "https://github.com/node-casbin/typeorm-adapter/issues" 72 | }, 73 | "lint-staged": { 74 | "*.{ts,tsx,js,jsx}": [ 75 | "eslint --fix", 76 | "git add" 77 | ] 78 | }, 79 | "jest": { 80 | "testEnvironmentOptions": { 81 | "url": "http://localhost" 82 | }, 83 | "transform": { 84 | "^.+\\.(ts|tsx)$": "ts-jest" 85 | }, 86 | "testMatch": [ 87 | "**/test/*.test.+(ts|tsx)" 88 | ], 89 | "moduleFileExtensions": [ 90 | "ts", 91 | "tsx", 92 | "js", 93 | "jsx", 94 | "json", 95 | "node" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/adapter.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Casbin Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the 'License'); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an 'AS IS' BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { Helper, Model, FilteredAdapter, UpdatableAdapter } from 'casbin'; 16 | import { CasbinRule } from './casbinRule'; 17 | import { 18 | DataSource, 19 | DataSourceOptions, 20 | FindOptionsWhere, 21 | Repository, 22 | } from 'typeorm'; 23 | import { CasbinMongoRule } from './casbinMongoRule'; 24 | 25 | type GenericCasbinRule = CasbinRule | CasbinMongoRule; 26 | type CasbinRuleConstructor = new (...args: any[]) => GenericCasbinRule; 27 | 28 | interface ExistentConnection { 29 | connection: DataSource; 30 | } 31 | export type TypeORMAdapterOptions = ExistentConnection | DataSourceOptions; 32 | 33 | export interface TypeORMAdapterConfig { 34 | customCasbinRuleEntity?: CasbinRuleConstructor; 35 | } 36 | 37 | /** 38 | * TypeORMAdapter represents the TypeORM filtered adapter for policy storage. 39 | */ 40 | export default class TypeORMAdapter 41 | implements FilteredAdapter, UpdatableAdapter 42 | { 43 | private adapterConfig?: TypeORMAdapterConfig; 44 | private option: DataSourceOptions; 45 | private typeorm: DataSource; 46 | private filtered = false; 47 | 48 | private constructor( 49 | option: TypeORMAdapterOptions, 50 | adapterConfig?: TypeORMAdapterConfig, 51 | ) { 52 | this.adapterConfig = adapterConfig; 53 | 54 | if ((option as ExistentConnection).connection) { 55 | this.typeorm = (option as ExistentConnection).connection; 56 | this.option = this.typeorm.options; 57 | } else { 58 | this.option = option as DataSourceOptions; 59 | } 60 | } 61 | 62 | public isFiltered(): boolean { 63 | return this.filtered; 64 | } 65 | 66 | /** 67 | * newAdapter is the constructor. 68 | * @param option typeorm connection option 69 | * @param adapterConfig additional configuration options for the adapter 70 | */ 71 | public static async newAdapter( 72 | option: TypeORMAdapterOptions, 73 | adapterConfig?: TypeORMAdapterConfig, 74 | ) { 75 | let a: TypeORMAdapter; 76 | 77 | const defaults = { 78 | synchronize: true, 79 | name: 'node-casbin-official', 80 | }; 81 | if ((option as ExistentConnection).connection) { 82 | a = new TypeORMAdapter(option, adapterConfig); 83 | } else { 84 | const options = option as DataSourceOptions; 85 | const entities = { 86 | entities: [ 87 | TypeORMAdapter.getCasbinRuleType(options.type, adapterConfig), 88 | ], 89 | }; 90 | const configuration = Object.assign(defaults, options); 91 | a = new TypeORMAdapter( 92 | Object.assign(configuration, entities), 93 | adapterConfig, 94 | ); 95 | } 96 | await a.open(); 97 | return a; 98 | } 99 | 100 | private async open() { 101 | if (!this.typeorm) { 102 | this.typeorm = new DataSource(this.option); 103 | } 104 | 105 | if (!this.typeorm.isInitialized) { 106 | await this.typeorm.initialize(); 107 | } 108 | } 109 | 110 | public async close() { 111 | if (this.typeorm.isInitialized) { 112 | await this.typeorm.destroy(); 113 | } 114 | } 115 | 116 | private async clearTable() { 117 | await this.getRepository().clear(); 118 | } 119 | 120 | private loadPolicyLine(line: GenericCasbinRule, model: Model) { 121 | const result = 122 | line.ptype + 123 | ', ' + 124 | [line.v0, line.v1, line.v2, line.v3, line.v4, line.v5, line.v6] 125 | .filter((n) => n) 126 | .map((n) => `"${n}"`) 127 | .join(', '); 128 | Helper.loadPolicyLine(result, model); 129 | } 130 | 131 | /** 132 | * loadPolicy loads all policy rules from the storage. 133 | */ 134 | public async loadPolicy(model: Model) { 135 | const lines = await this.getRepository().find(); 136 | 137 | for (const line of lines) { 138 | this.loadPolicyLine(line, model); 139 | } 140 | } 141 | 142 | // Loading policies based on filter condition 143 | public async loadFilteredPolicy( 144 | model: Model, 145 | filter: FindOptionsWhere, 146 | ) { 147 | const filteredLines = await this.getRepository().find({ where: filter }); 148 | for (const line of filteredLines) { 149 | this.loadPolicyLine(line, model); 150 | } 151 | this.filtered = true; 152 | } 153 | 154 | private savePolicyLine(ptype: string, rule: string[]): GenericCasbinRule { 155 | const line = new (this.getCasbinRuleConstructor())(); 156 | 157 | line.ptype = ptype; 158 | if (rule.length > 0) { 159 | line.v0 = rule[0]; 160 | } 161 | if (rule.length > 1) { 162 | line.v1 = rule[1]; 163 | } 164 | if (rule.length > 2) { 165 | line.v2 = rule[2]; 166 | } 167 | if (rule.length > 3) { 168 | line.v3 = rule[3]; 169 | } 170 | if (rule.length > 4) { 171 | line.v4 = rule[4]; 172 | } 173 | if (rule.length > 5) { 174 | line.v5 = rule[5]; 175 | } 176 | if (rule.length > 6) { 177 | line.v6 = rule[6]; 178 | } 179 | 180 | return line; 181 | } 182 | 183 | /** 184 | * savePolicy saves all policy rules to the storage. 185 | */ 186 | public async savePolicy(model: Model) { 187 | await this.clearTable(); 188 | 189 | let astMap = model.model.get('p'); 190 | const lines: GenericCasbinRule[] = []; 191 | // @ts-ignore 192 | for (const [ptype, ast] of astMap) { 193 | for (const rule of ast.policy) { 194 | const line = this.savePolicyLine(ptype, rule); 195 | lines.push(line); 196 | } 197 | } 198 | 199 | astMap = model.model.get('g'); 200 | if (astMap) { 201 | // @ts-ignore 202 | for (const [ptype, ast] of astMap) { 203 | for (const rule of ast.policy) { 204 | const line = this.savePolicyLine(ptype, rule); 205 | lines.push(line); 206 | } 207 | } 208 | } 209 | 210 | const queryRunner = this.typeorm.createQueryRunner(); 211 | 212 | await queryRunner.connect(); 213 | await queryRunner.startTransaction(); 214 | 215 | try { 216 | await queryRunner.manager.save(lines); 217 | await queryRunner.commitTransaction(); 218 | } catch (err) { 219 | await queryRunner.rollbackTransaction(); 220 | throw err; 221 | } finally { 222 | await queryRunner.release(); 223 | } 224 | 225 | return true; 226 | } 227 | 228 | /** 229 | * addPolicy adds a policy rule to the storage. 230 | */ 231 | public async addPolicy(sec: string, ptype: string, rule: string[]) { 232 | const line = this.savePolicyLine(ptype, rule); 233 | await this.getRepository().save(line); 234 | } 235 | 236 | /** 237 | * addPolicies adds policy rules to the storage. 238 | */ 239 | public async addPolicies(sec: string, ptype: string, rules: string[][]) { 240 | const lines: GenericCasbinRule[] = []; 241 | for (const rule of rules) { 242 | const line = this.savePolicyLine(ptype, rule); 243 | lines.push(line); 244 | } 245 | 246 | const queryRunner = this.typeorm.createQueryRunner(); 247 | 248 | await queryRunner.connect(); 249 | await queryRunner.startTransaction(); 250 | 251 | try { 252 | await queryRunner.manager.save(lines); 253 | await queryRunner.commitTransaction(); 254 | } catch (err) { 255 | await queryRunner.rollbackTransaction(); 256 | throw err; 257 | } finally { 258 | await queryRunner.release(); 259 | } 260 | } 261 | 262 | async updatePolicy( 263 | sec: string, 264 | ptype: string, 265 | oldRule: string[], 266 | newRule: string[], 267 | ): Promise { 268 | const { v0, v1, v2, v3, v4, v5, v6 } = this.savePolicyLine(ptype, oldRule); 269 | const newLine = this.savePolicyLine(ptype, newRule); 270 | 271 | const foundLine = await this.getRepository().findOneOrFail({ 272 | where: { 273 | ptype, 274 | v0, 275 | v1, 276 | v2, 277 | v3, 278 | v4, 279 | v5, 280 | v6, 281 | }, 282 | }); 283 | 284 | await this.getRepository().save(Object.assign(foundLine, newLine)); 285 | } 286 | 287 | /** 288 | * removePolicy removes a policy rule from the storage. 289 | */ 290 | public async removePolicy(sec: string, ptype: string, rule: string[]) { 291 | const line = this.savePolicyLine(ptype, rule); 292 | await this.getRepository().delete({ 293 | ...line, 294 | }); 295 | } 296 | 297 | /** 298 | * removePolicies removes policy rules from the storage. 299 | */ 300 | public async removePolicies(sec: string, ptype: string, rules: string[][]) { 301 | const queryRunner = this.typeorm.createQueryRunner(); 302 | const type = TypeORMAdapter.getCasbinRuleType( 303 | this.option.type, 304 | this.adapterConfig, 305 | ); 306 | 307 | await queryRunner.connect(); 308 | await queryRunner.startTransaction(); 309 | 310 | try { 311 | for (const rule of rules) { 312 | const line = this.savePolicyLine(ptype, rule); 313 | await queryRunner.manager.delete(type, line); 314 | } 315 | await queryRunner.commitTransaction(); 316 | } catch (err) { 317 | await queryRunner.rollbackTransaction(); 318 | throw err; 319 | } finally { 320 | await queryRunner.release(); 321 | } 322 | } 323 | 324 | /** 325 | * removeFilteredPolicy removes policy rules that match the filter from the storage. 326 | */ 327 | public async removeFilteredPolicy( 328 | sec: string, 329 | ptype: string, 330 | fieldIndex: number, 331 | ...fieldValues: string[] 332 | ) { 333 | const line = new (this.getCasbinRuleConstructor())(); 334 | 335 | if (ptype) { 336 | line.ptype = ptype; 337 | } 338 | 339 | if (fieldIndex <= 0 && 0 < fieldIndex + fieldValues.length) { 340 | if (fieldValues[0 - fieldIndex]) { 341 | line.v0 = fieldValues[0 - fieldIndex]; 342 | } 343 | } 344 | if (fieldIndex <= 1 && 1 < fieldIndex + fieldValues.length) { 345 | if (fieldValues[1 - fieldIndex]) { 346 | line.v1 = fieldValues[1 - fieldIndex]; 347 | } 348 | } 349 | if (fieldIndex <= 2 && 2 < fieldIndex + fieldValues.length) { 350 | if (fieldValues[2 - fieldIndex]) { 351 | line.v2 = fieldValues[2 - fieldIndex]; 352 | } 353 | } 354 | if (fieldIndex <= 3 && 3 < fieldIndex + fieldValues.length) { 355 | if (fieldValues[3 - fieldIndex]) { 356 | line.v3 = fieldValues[3 - fieldIndex]; 357 | } 358 | } 359 | if (fieldIndex <= 4 && 4 < fieldIndex + fieldValues.length) { 360 | if (fieldValues[4 - fieldIndex]) { 361 | line.v4 = fieldValues[4 - fieldIndex]; 362 | } 363 | } 364 | if (fieldIndex <= 5 && 5 < fieldIndex + fieldValues.length) { 365 | if (fieldValues[5 - fieldIndex]) { 366 | line.v5 = fieldValues[5 - fieldIndex]; 367 | } 368 | } 369 | if (fieldIndex <= 6 && 6 < fieldIndex + fieldValues.length) { 370 | if (fieldValues[6 - fieldIndex]) { 371 | line.v6 = fieldValues[6 - fieldIndex]; 372 | } 373 | } 374 | 375 | await this.getRepository().delete({ 376 | ...line, 377 | }); 378 | } 379 | 380 | private getCasbinRuleConstructor(): CasbinRuleConstructor { 381 | return TypeORMAdapter.getCasbinRuleType( 382 | this.option.type, 383 | this.adapterConfig, 384 | ); 385 | } 386 | 387 | /** 388 | * Returns either a {@link CasbinRule} or a {@link CasbinMongoRule}, depending on the type. If passed a custom entity through the adapter config it will use that entity type. 389 | * This switch is required as the normal {@link CasbinRule} does not work when using MongoDB as a backend (due to a missing ObjectId field). 390 | * @param type 391 | * @param adapterConfig 392 | */ 393 | private static getCasbinRuleType( 394 | type: string, 395 | adapterConfig?: TypeORMAdapterConfig, 396 | ): CasbinRuleConstructor { 397 | if (adapterConfig?.customCasbinRuleEntity) { 398 | return adapterConfig.customCasbinRuleEntity; 399 | } 400 | 401 | if (type === 'mongodb') { 402 | return CasbinMongoRule; 403 | } 404 | return CasbinRule; 405 | } 406 | 407 | private getRepository(): Repository { 408 | return this.typeorm.getRepository(this.getCasbinRuleConstructor()); 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/casbinMongoRule.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Casbin Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { BaseEntity, Column, Entity, ObjectId, ObjectIdColumn } from 'typeorm'; 16 | 17 | @Entity() 18 | export class CasbinMongoRule extends BaseEntity { 19 | @ObjectIdColumn() 20 | public id!: ObjectId; 21 | 22 | @Column({ 23 | nullable: true, 24 | }) 25 | public ptype!: string; 26 | 27 | @Column({ 28 | nullable: true, 29 | }) 30 | public v0!: string; 31 | 32 | @Column({ 33 | nullable: true, 34 | }) 35 | public v1!: string; 36 | 37 | @Column({ 38 | nullable: true, 39 | }) 40 | public v2!: string; 41 | 42 | @Column({ 43 | nullable: true, 44 | }) 45 | public v3!: string; 46 | 47 | @Column({ 48 | nullable: true, 49 | }) 50 | public v4!: string; 51 | 52 | @Column({ 53 | nullable: true, 54 | }) 55 | public v5!: string; 56 | 57 | @Column({ 58 | nullable: true, 59 | }) 60 | public v6!: string; 61 | } 62 | -------------------------------------------------------------------------------- /src/casbinRule.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Casbin Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'; 16 | 17 | @Entity() 18 | export class CasbinRule extends BaseEntity { 19 | @PrimaryGeneratedColumn() 20 | public id: number; 21 | 22 | @Column({ 23 | nullable: true, 24 | }) 25 | public ptype: string; 26 | 27 | @Column({ 28 | nullable: true, 29 | }) 30 | public v0: string; 31 | 32 | @Column({ 33 | nullable: true, 34 | }) 35 | public v1: string; 36 | 37 | @Column({ 38 | nullable: true, 39 | }) 40 | public v2: string; 41 | 42 | @Column({ 43 | nullable: true, 44 | }) 45 | public v3: string; 46 | 47 | @Column({ 48 | nullable: true, 49 | }) 50 | public v4: string; 51 | 52 | @Column({ 53 | nullable: true, 54 | }) 55 | public v5: string; 56 | 57 | @Column({ 58 | nullable: true, 59 | }) 60 | public v6: string; 61 | } 62 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export { TypeORMAdapterOptions } from './adapter'; 3 | export * from './casbinMongoRule'; 4 | export * from './casbinRule'; 5 | 6 | import TypeORMAdapter from './adapter'; 7 | 8 | export default TypeORMAdapter; 9 | -------------------------------------------------------------------------------- /test/adapter-config.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Casbin Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { Enforcer, setDefaultFileSystem } from 'casbin'; 16 | import { 17 | CreateDateColumn, 18 | DataSource, 19 | Entity, 20 | EntityNotFoundError, 21 | UpdateDateColumn, 22 | } from 'typeorm'; 23 | import TypeORMAdapter, { CasbinRule } from '../src/index'; 24 | import { connectionConfig } from './config'; 25 | 26 | import * as fs from 'fs'; 27 | setDefaultFileSystem(fs as any); 28 | 29 | @Entity('custom_rule') 30 | class CustomCasbinRule extends CasbinRule { 31 | @CreateDateColumn() 32 | public createdDate: Date; 33 | 34 | @UpdateDateColumn() 35 | public updatedDate: Date; 36 | } 37 | 38 | test( 39 | 'TestAdapter', 40 | async () => { 41 | const datasource = new DataSource({ 42 | ...connectionConfig, 43 | entities: [CustomCasbinRule], 44 | synchronize: true, 45 | }); 46 | 47 | const a = await TypeORMAdapter.newAdapter( 48 | { connection: datasource }, 49 | { 50 | customCasbinRuleEntity: CustomCasbinRule, 51 | }, 52 | ); 53 | try { 54 | // Because the DB is empty at first, 55 | // so we need to load the policy from the file adapter (.CSV) first. 56 | const e = new Enforcer(); 57 | 58 | await e.initWithFile( 59 | 'examples/rbac_model.conf', 60 | 'examples/rbac_policy.csv', 61 | ); 62 | 63 | // This is a trick to save the current policy to the DB. 64 | // We can't call e.savePolicy() because the adapter in the enforcer is still the file adapter. 65 | // The current policy means the policy in the Node-Casbin enforcer (aka in memory). 66 | await a.savePolicy(e.getModel()); 67 | 68 | const repository = datasource.getRepository(CustomCasbinRule); 69 | const rules = await repository.find(); 70 | expect(rules[0].createdDate).not.toBeFalsy(); 71 | expect(rules[0].updatedDate).not.toBeFalsy(); 72 | 73 | // Verify update method works 74 | const initialPolicy = ['bob', 'data3', 'write']; 75 | const updatedPolicy = ['bob', 'data3', 'read']; 76 | const getCurrentPolicyLinesFromDB = async () => { 77 | const policyRow = await repository.findOneByOrFail({ 78 | v0: initialPolicy[0], 79 | v1: initialPolicy[1], 80 | }); 81 | return [policyRow.v0, policyRow.v1, policyRow.v2]; 82 | }; 83 | 84 | await a.addPolicy('', 'p', initialPolicy); 85 | expect(await getCurrentPolicyLinesFromDB()).toMatchObject(initialPolicy); 86 | 87 | await a.updatePolicy('', 'p', initialPolicy, updatedPolicy); 88 | expect(await getCurrentPolicyLinesFromDB()).toMatchObject(updatedPolicy); 89 | 90 | // We expect that we won't find a read policy anymore 91 | await expect( 92 | repository.findOneByOrFail({ 93 | v0: initialPolicy[0], 94 | v1: initialPolicy[1], 95 | v2: initialPolicy[2], 96 | }), 97 | ).rejects.toThrow(EntityNotFoundError); 98 | } finally { 99 | a.close(); 100 | } 101 | }, 102 | 60 * 1000, 103 | ); 104 | -------------------------------------------------------------------------------- /test/adapter-with-acl-model.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Casbin Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Enforcer, setDefaultFileSystem} from 'casbin'; 16 | import TypeORMAdapter from '../src/index'; 17 | import { connectionConfig } from './config'; 18 | 19 | import * as fs from 'fs'; 20 | setDefaultFileSystem(fs as any); 21 | 22 | test( 23 | 'TestAdapter', 24 | async () => { 25 | const a = await TypeORMAdapter.newAdapter(connectionConfig); 26 | try { 27 | // Because the DB is empty at first, 28 | // so we need to load the policy from the file adapter (.CSV) first. 29 | let e = new Enforcer(); 30 | 31 | await e.initWithFile( 32 | 'examples/acl_model.conf', 33 | 'examples/acl_policy.csv', 34 | ); 35 | 36 | // This is a trick to save the current policy to the DB. 37 | // We can't call e.savePolicy() because the adapter in the enforcer is still the file adapter. 38 | // The current policy means the policy in the Node-Casbin enforcer (aka in memory). 39 | await a.savePolicy(e.getModel()); 40 | 41 | // Clear the current policy. 42 | e.clearPolicy(); 43 | expect(await e.getPolicy()).toEqual([]); 44 | 45 | // Load the policy from DB. 46 | await a.loadPolicy(e.getModel()); 47 | expect(await e.getPolicy()).toEqual([ 48 | ['alice', 'data1', 'read'], 49 | ['bob', 'data2', 'write'], 50 | ]); 51 | 52 | // Note: you don't need to look at the above code 53 | // if you already have a working DB with policy inside. 54 | 55 | // Now the DB has policy, so we can provide a normal use case. 56 | // Create an adapter and an enforcer. 57 | // newEnforcer() will load the policy automatically. 58 | e = new Enforcer(); 59 | await e.initWithAdapter('examples/rbac_model.conf', a); 60 | expect(await e.getPolicy()).toEqual([ 61 | ['alice', 'data1', 'read'], 62 | ['bob', 'data2', 'write'], 63 | ]); 64 | 65 | // load filtered policies 66 | e.clearPolicy(); 67 | await a.loadFilteredPolicy(e.getModel(), { ptype: 'p', v0: 'alice' }); 68 | expect(await e.getFilteredNamedPolicy('p', 0, 'alice')).toEqual([ 69 | ['alice', 'data1', 'read'], 70 | ]); 71 | 72 | // Add policy to DB 73 | await a.addPolicy('', 'p', ['role', 'res', 'action']); 74 | e = new Enforcer(); 75 | await e.initWithAdapter('examples/rbac_model.conf', a); 76 | expect(await e.getPolicy()).toEqual([ 77 | ['alice', 'data1', 'read'], 78 | ['bob', 'data2', 'write'], 79 | ['role', 'res', 'action'], 80 | ]); 81 | 82 | await a.addPolicies('', 'p', [ 83 | ['role1', 'res1', 'action1'], 84 | ['role2', 'res2', 'action2'], 85 | ['role3', 'res3', 'action3'], 86 | ['role4', 'res4', 'action4'], 87 | ['role5', 'res5', 'action5'], 88 | ]); 89 | e = new Enforcer(); 90 | await e.initWithAdapter('examples/rbac_model.conf', a); 91 | expect(await e.getPolicy()).toEqual([ 92 | ['alice', 'data1', 'read'], 93 | ['bob', 'data2', 'write'], 94 | ['role', 'res', 'action'], 95 | ['role1', 'res1', 'action1'], 96 | ['role2', 'res2', 'action2'], 97 | ['role3', 'res3', 'action3'], 98 | ['role4', 'res4', 'action4'], 99 | ['role5', 'res5', 'action5'], 100 | ]); 101 | 102 | // Remove policy from DB 103 | await a.removePolicy('', 'p', ['role', 'res', 'action']); 104 | e = new Enforcer(); 105 | await e.initWithAdapter('examples/rbac_model.conf', a); 106 | expect(await e.getPolicy()).toEqual([ 107 | ['alice', 'data1', 'read'], 108 | ['bob', 'data2', 'write'], 109 | ['role1', 'res1', 'action1'], 110 | ['role2', 'res2', 'action2'], 111 | ['role3', 'res3', 'action3'], 112 | ['role4', 'res4', 'action4'], 113 | ['role5', 'res5', 'action5'], 114 | ]); 115 | 116 | await a.removePolicies('', 'p', [ 117 | ['role1', 'res1', 'action1'], 118 | ['role2', 'res2', 'action2'], 119 | ['role3', 'res3', 'action3'], 120 | ['role4', 'res4', 'action4'], 121 | ['role5', 'res5', 'action5'], 122 | ]); 123 | e = new Enforcer(); 124 | await e.initWithAdapter('examples/rbac_model.conf', a); 125 | expect(await e.getPolicy()).toEqual([ 126 | ['alice', 'data1', 'read'], 127 | ['bob', 'data2', 'write'], 128 | ]); 129 | } finally { 130 | a.close(); 131 | } 132 | }, 133 | 60 * 1000, 134 | ); 135 | -------------------------------------------------------------------------------- /test/adapter-with-rbac-model.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Casbin Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Enforcer, setDefaultFileSystem} from 'casbin'; 16 | import TypeORMAdapter from '../src/index'; 17 | import { connectionConfig } from './config'; 18 | 19 | import * as fs from 'fs'; 20 | setDefaultFileSystem(fs as any); 21 | 22 | test( 23 | 'TestAdapter', 24 | async () => { 25 | const a = await TypeORMAdapter.newAdapter(connectionConfig); 26 | try { 27 | // Because the DB is empty at first, 28 | // so we need to load the policy from the file adapter (.CSV) first. 29 | let e = new Enforcer(); 30 | 31 | await e.initWithFile( 32 | 'examples/rbac_model.conf', 33 | 'examples/rbac_policy.csv', 34 | ); 35 | 36 | // This is a trick to save the current policy to the DB. 37 | // We can't call e.savePolicy() because the adapter in the enforcer is still the file adapter. 38 | // The current policy means the policy in the Node-Casbin enforcer (aka in memory). 39 | await a.savePolicy(e.getModel()); 40 | 41 | // Clear the current policy. 42 | e.clearPolicy(); 43 | expect(await e.getPolicy()).toEqual([]); 44 | 45 | // Load the policy from DB. 46 | await a.loadPolicy(e.getModel()); 47 | expect(await e.getPolicy()).toEqual([ 48 | ['alice', 'data1', 'read'], 49 | ['bob', 'data2', 'write'], 50 | ['data2_admin', 'data2', 'read'], 51 | ['data2_admin', 'data2', 'write'], 52 | ]); 53 | 54 | // Note: you don't need to look at the above code 55 | // if you already have a working DB with policy inside. 56 | 57 | // Now the DB has policy, so we can provide a normal use case. 58 | // Create an adapter and an enforcer. 59 | // newEnforcer() will load the policy automatically. 60 | e = new Enforcer(); 61 | await e.initWithAdapter('examples/rbac_model.conf', a); 62 | expect(await e.getPolicy()).toEqual([ 63 | ['alice', 'data1', 'read'], 64 | ['bob', 'data2', 'write'], 65 | ['data2_admin', 'data2', 'read'], 66 | ['data2_admin', 'data2', 'write'], 67 | ]); 68 | 69 | // load filtered policies 70 | e.clearPolicy(); 71 | await a.loadFilteredPolicy(e.getModel(), { ptype: 'p', v0: 'alice' }); 72 | expect(await e.getFilteredNamedPolicy('p', 0, 'alice')).toEqual([ 73 | ['alice', 'data1', 'read'], 74 | ]); 75 | 76 | // Add policy to DB 77 | await a.addPolicy('', 'p', ['role', 'res', 'action']); 78 | e = new Enforcer(); 79 | await e.initWithAdapter('examples/rbac_model.conf', a); 80 | expect(await e.getPolicy()).toEqual([ 81 | ['alice', 'data1', 'read'], 82 | ['bob', 'data2', 'write'], 83 | ['data2_admin', 'data2', 'read'], 84 | ['data2_admin', 'data2', 'write'], 85 | ['role', 'res', 'action'], 86 | ]); 87 | 88 | await a.addPolicies('', 'p', [ 89 | ['role1', 'res1', 'action1'], 90 | ['role2', 'res2', 'action2'], 91 | ['role3', 'res3', 'action3'], 92 | ['role4', 'res4', 'action4'], 93 | ['role5', 'res5', 'action5'], 94 | ]); 95 | e = new Enforcer(); 96 | await e.initWithAdapter('examples/rbac_model.conf', a); 97 | expect(await e.getPolicy()).toEqual([ 98 | ['alice', 'data1', 'read'], 99 | ['bob', 'data2', 'write'], 100 | ['data2_admin', 'data2', 'read'], 101 | ['data2_admin', 'data2', 'write'], 102 | ['role', 'res', 'action'], 103 | ['role1', 'res1', 'action1'], 104 | ['role2', 'res2', 'action2'], 105 | ['role3', 'res3', 'action3'], 106 | ['role4', 'res4', 'action4'], 107 | ['role5', 'res5', 'action5'], 108 | ]); 109 | 110 | // Remove policy from DB 111 | await a.removePolicy('', 'p', ['role', 'res', 'action']); 112 | e = new Enforcer(); 113 | await e.initWithAdapter('examples/rbac_model.conf', a); 114 | expect(await e.getPolicy()).toEqual([ 115 | ['alice', 'data1', 'read'], 116 | ['bob', 'data2', 'write'], 117 | ['data2_admin', 'data2', 'read'], 118 | ['data2_admin', 'data2', 'write'], 119 | ['role1', 'res1', 'action1'], 120 | ['role2', 'res2', 'action2'], 121 | ['role3', 'res3', 'action3'], 122 | ['role4', 'res4', 'action4'], 123 | ['role5', 'res5', 'action5'], 124 | ]); 125 | 126 | await a.removePolicies('', 'p', [ 127 | ['role1', 'res1', 'action1'], 128 | ['role2', 'res2', 'action2'], 129 | ['role3', 'res3', 'action3'], 130 | ['role4', 'res4', 'action4'], 131 | ['role5', 'res5', 'action5'], 132 | ]); 133 | e = new Enforcer(); 134 | await e.initWithAdapter('examples/rbac_model.conf', a); 135 | expect(await e.getPolicy()).toEqual([ 136 | ['alice', 'data1', 'read'], 137 | ['bob', 'data2', 'write'], 138 | ['data2_admin', 'data2', 'read'], 139 | ['data2_admin', 'data2', 'write'], 140 | ]); 141 | } finally { 142 | a.close(); 143 | } 144 | }, 145 | 60 * 1000, 146 | ); 147 | -------------------------------------------------------------------------------- /test/config.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceOptions } from 'typeorm'; 2 | 3 | const SHOULD_USE_MYSQL = 4 | process.env.MYSQL_USER != null || 5 | process.env.MYSQL_PORT != null || 6 | process.env.MYSQL_PASSWORD != null || 7 | process.env.MYSQL_DB != null; 8 | 9 | export const connectionConfig: DataSourceOptions = SHOULD_USE_MYSQL 10 | ? { 11 | type: 'mysql', 12 | host: 'localhost', 13 | port: parseInt(process.env.MYSQL_PORT || '', 10) || 3306, 14 | username: process.env.MYSQL_USER || 'root', 15 | password: 16 | process.env.MYSQL_PASSWORD !== undefined 17 | ? process.env.MYSQL_PASSWORD === '' 18 | ? undefined 19 | : process.env.MYSQL_PASSWORD 20 | : 'password', 21 | database: process.env.MYSQL_DB || 'casbin', 22 | dropSchema: true, 23 | } 24 | : { 25 | type: 'sqlite', 26 | database: ':memory:', 27 | dropSchema: true, 28 | }; 29 | -------------------------------------------------------------------------------- /test/existent-connection-adapter.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Casbin Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Enforcer, setDefaultFileSystem, Util} from 'casbin'; 16 | import { DataSource } from 'typeorm'; 17 | import TypeORMAdapter, { CasbinRule } from '../src/index'; 18 | import { connectionConfig } from './config'; 19 | 20 | import * as fs from 'fs'; 21 | setDefaultFileSystem(fs as any); 22 | 23 | async function testGetPolicy(e: Enforcer, res: string[][]) { 24 | const myRes = await e.getPolicy(); 25 | 26 | expect(Util.array2DEquals(res, myRes)).toBe(true); 27 | } 28 | 29 | async function testGetFilteredPolicy(e: Enforcer, res: string[]) { 30 | const filtered = await e.getFilteredNamedPolicy('p', 0, 'alice'); 31 | const myRes = filtered[0]; 32 | 33 | expect(Util.arrayEquals(res, myRes)).toBe(true); 34 | } 35 | 36 | test( 37 | 'TestAdapter', 38 | async () => { 39 | const connection = await new DataSource({ 40 | ...connectionConfig, 41 | entities: [CasbinRule], 42 | synchronize: true, 43 | }); 44 | const a = await TypeORMAdapter.newAdapter({ connection }); 45 | try { 46 | // Because the DB is empty at first, 47 | // so we need to load the policy from the file adapter (.CSV) first. 48 | let e = new Enforcer(); 49 | 50 | await e.initWithFile( 51 | 'examples/rbac_model.conf', 52 | 'examples/rbac_policy.csv', 53 | ); 54 | 55 | // This is a trick to save the current policy to the DB. 56 | // We can't call e.savePolicy() because the adapter in the enforcer is still the file adapter. 57 | // The current policy means the policy in the Node-Casbin enforcer (aka in memory). 58 | await a.savePolicy(e.getModel()); 59 | 60 | // Clear the current policy. 61 | e.clearPolicy(); 62 | await testGetPolicy(e, []); 63 | 64 | // Load the policy from DB. 65 | await a.loadPolicy(e.getModel()); 66 | await testGetPolicy(e, [ 67 | ['alice', 'data1', 'read'], 68 | ['bob', 'data2', 'write'], 69 | ['data2_admin', 'data2', 'read'], 70 | ['data2_admin', 'data2', 'write'], 71 | ]); 72 | 73 | // Note: you don't need to look at the above code 74 | // if you already have a working DB with policy inside. 75 | 76 | // Now the DB has policy, so we can provide a normal use case. 77 | // Create an adapter and an enforcer. 78 | // newEnforcer() will load the policy automatically. 79 | e = new Enforcer(); 80 | await e.initWithAdapter('examples/rbac_model.conf', a); 81 | await testGetPolicy(e, [ 82 | ['alice', 'data1', 'read'], 83 | ['bob', 'data2', 'write'], 84 | ['data2_admin', 'data2', 'read'], 85 | ['data2_admin', 'data2', 'write'], 86 | ]); 87 | 88 | // load filtered policies 89 | await a.loadFilteredPolicy(e.getModel(), { ptype: 'p', v0: 'alice' }); 90 | await testGetFilteredPolicy(e, ['alice', 'data1', 'read']); 91 | 92 | // Add policy to DB 93 | await a.addPolicy('', 'p', ['role', 'res', 'action']); 94 | e = new Enforcer(); 95 | await e.initWithAdapter('examples/rbac_model.conf', a); 96 | await testGetPolicy(e, [ 97 | ['alice', 'data1', 'read'], 98 | ['bob', 'data2', 'write'], 99 | ['data2_admin', 'data2', 'read'], 100 | ['data2_admin', 'data2', 'write'], 101 | ['role', 'res', 'action'], 102 | ]); 103 | 104 | await a.addPolicies('', 'p', [ 105 | ['role1', 'res1', 'action1'], 106 | ['role2', 'res2', 'action2'], 107 | ['role3', 'res3', 'action3'], 108 | ['role4', 'res4', 'action4'], 109 | ['role5', 'res5', 'action5'], 110 | ]); 111 | e = new Enforcer(); 112 | await e.initWithAdapter('examples/rbac_model.conf', a); 113 | await testGetPolicy(e, [ 114 | ['alice', 'data1', 'read'], 115 | ['bob', 'data2', 'write'], 116 | ['data2_admin', 'data2', 'read'], 117 | ['data2_admin', 'data2', 'write'], 118 | ['role', 'res', 'action'], 119 | ['role1', 'res1', 'action1'], 120 | ['role2', 'res2', 'action2'], 121 | ['role3', 'res3', 'action3'], 122 | ['role4', 'res4', 'action4'], 123 | ['role5', 'res5', 'action5'], 124 | ]); 125 | 126 | // Remove policy from DB 127 | await a.removePolicy('', 'p', ['role', 'res', 'action']); 128 | e = new Enforcer(); 129 | await e.initWithAdapter('examples/rbac_model.conf', a); 130 | await testGetPolicy(e, [ 131 | ['alice', 'data1', 'read'], 132 | ['bob', 'data2', 'write'], 133 | ['data2_admin', 'data2', 'read'], 134 | ['data2_admin', 'data2', 'write'], 135 | ['role1', 'res1', 'action1'], 136 | ['role2', 'res2', 'action2'], 137 | ['role3', 'res3', 'action3'], 138 | ['role4', 'res4', 'action4'], 139 | ['role5', 'res5', 'action5'], 140 | ]); 141 | 142 | await a.removePolicies('', 'p', [ 143 | ['role1', 'res1', 'action1'], 144 | ['role2', 'res2', 'action2'], 145 | ['role3', 'res3', 'action3'], 146 | ['role4', 'res4', 'action4'], 147 | ['role5', 'res5', 'action5'], 148 | ]); 149 | e = new Enforcer(); 150 | await e.initWithAdapter('examples/rbac_model.conf', a); 151 | await testGetPolicy(e, [ 152 | ['alice', 'data1', 'read'], 153 | ['bob', 'data2', 'write'], 154 | ['data2_admin', 'data2', 'read'], 155 | ['data2_admin', 'data2', 'write'], 156 | ]); 157 | } finally { 158 | a.close(); 159 | } 160 | }, 161 | 60 * 1000, 162 | ); 163 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "declarationDir": "lib", 8 | "strict": true, 9 | "outDir": "lib", 10 | "strictPropertyInitialization": false, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true 13 | }, 14 | "include": ["src/**/*.ts"] 15 | } 16 | --------------------------------------------------------------------------------