├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── .releaserc ├── LICENSE ├── README.md ├── examples ├── rbac_model.conf └── rbac_policy.csv ├── package-lock.json ├── package.json ├── prisma ├── casbin_rule.prisma ├── casbin_rule.sql └── schema.prisma ├── src └── adapter.ts ├── test └── adapter.test.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | "prettier/@typescript-eslint", 9 | "eslint:recommended" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/array-type": ["error", { "default": "array-simple" }], 13 | "@typescript-eslint/explicit-member-accessibility": ["off"], 14 | "@typescript-eslint/no-non-null-assertion": ["off"], 15 | "@typescript-eslint/no-use-before-define": ["off"], 16 | "@typescript-eslint/no-parameter-properties": ["off"], 17 | "@typescript-eslint/no-unused-vars": [ 18 | "error", 19 | { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } 20 | ], 21 | "@typescript-eslint/ban-ts-comment": ["off"], 22 | "@typescript-eslint/no-empty-function": ["off"], 23 | "@typescript-eslint/explicit-function-return-type": ["off"], 24 | "@typescript-eslint/no-explicit-any": ["off"], 25 | "no-unused-vars": ["off"] 26 | }, 27 | "env": { 28 | "node": true, 29 | "jest": true, 30 | "es6":true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | services: 10 | mysql: 11 | image: mysql:latest 12 | ports: 13 | - 3306:3306 14 | env: 15 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 16 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 20 23 | - run: mysql --host 127.0.0.1 --port 3306 -uroot -p -e "source ./prisma/casbin_rule.sql" 24 | - run: npm i 25 | - run: npx prisma generate 26 | - run: npm run format:check 27 | - run: npm run lint 28 | - run: npx jest --coverage --forceExit 29 | - name: Coveralls Parallel 30 | uses: coverallsapp/github-action@master 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | finish: 35 | needs: build 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Coveralls Finished 39 | uses: coverallsapp/github-action@master 40 | with: 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | parallel-finished: true 43 | - uses: actions/checkout@v2 44 | - run: npm i 45 | - run: npm run build 46 | - name: Release 47 | if: github.event_name == 'push' && github.repository == 'node-casbin/prisma-adapter' 48 | run: npx semantic-release 49 | env: 50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true, 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/npm", 7 | [ 8 | "@semantic-release/git", 9 | { 10 | "assets": [ 11 | "package.json" 12 | ], 13 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 14 | } 15 | ], 16 | "@semantic-release/github" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /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 [2020] [Casbin] 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 | # Prisma Adapter 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![NPM download][download-image]][download-url] 5 | [![Build Status][ci-image]][ci-url] 6 | [![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN) 7 | 8 | [npm-image]: https://img.shields.io/npm/v/casbin-prisma-adapter.svg 9 | [npm-url]: https://npmjs.org/package/casbin-prisma-adapter 10 | [download-image]: https://img.shields.io/npm/dm/casbin-prisma-adapter.svg 11 | [download-url]: https://npmjs.org/package/casbin-prisma-adapter 12 | [ci-image]: https://github.com/node-casbin/prisma-adapter/workflows/ci/badge.svg?branch=master 13 | [ci-url]: https://github.com/node-casbin/prisma-adapter/actions 14 | 15 | Prisma Adapter is the [Prisma](https://github.com/prisma/prisma) adapter for [Node-Casbin](https://github.com/casbin/node-casbin). With this library, Node-Casbin can load policy from Prisma supported database or save policy to it. 16 | 17 | Based on [Officially Supported Databases](https://www.prisma.io/docs/), the current supported databases are: 18 | 19 | - PostgreSQL 20 | - MySQL 21 | - SQLite 22 | - MongoDB 23 | 24 | You may find other 3rd-party supported DBs in Prisma website or other places. 25 | 26 | ## Installation 27 | 28 | ``` 29 | npm install casbin-prisma-adapter --save 30 | ``` 31 | 32 | ## Getting Started 33 | 34 | Append the following content to your `schema.prisma`: 35 | 36 | ```prisma 37 | model CasbinRule { 38 | id Int @id @default(autoincrement()) 39 | ptype String 40 | v0 String? 41 | v1 String? 42 | v2 String? 43 | v3 String? 44 | v4 String? 45 | v5 String? 46 | 47 | @@map("casbin_rule") 48 | } 49 | ``` 50 | 51 | Create table(MySQL): 52 | 53 | ```sql 54 | CREATE TABLE IF NOT EXISTS `casbin_rule` ( 55 | `id` int NOT NULL AUTO_INCREMENT, 56 | `ptype` varchar(255) DEFAULT NULL, 57 | `v0` varchar(255) DEFAULT NULL, 58 | `v1` varchar(255) DEFAULT NULL, 59 | `v2` varchar(255) DEFAULT NULL, 60 | `v3` varchar(255) DEFAULT NULL, 61 | `v4` varchar(255) DEFAULT NULL, 62 | `v5` varchar(255) DEFAULT NULL, 63 | PRIMARY KEY (`id`) 64 | ); 65 | ``` 66 | 67 | Here is a simple example: 68 | 69 | ```ts 70 | import casbin from 'casbin'; 71 | import { PrismaAdapter } from 'casbin-prisma-adapter'; 72 | 73 | async function main() { 74 | const a = await PrismaAdapter.newAdapter(); 75 | const e = await casbin.newEnforcer('examples/rbac_model.conf', a); 76 | 77 | // Check the permission. 78 | e.enforce('alice', 'data1', 'read'); 79 | 80 | // Modify the policy. 81 | // await e.addPolicy(...); 82 | // await e.removePolicy(...); 83 | 84 | // Save the policy back to DB. 85 | await e.savePolicy(); 86 | } 87 | 88 | main(); 89 | ``` 90 | 91 | ## Getting Help 92 | 93 | - [Node-Casbin](https://github.com/casbin/node-casbin) 94 | 95 | ## License 96 | 97 | This project is under Apache 2.0 License. See the [LICENSE](LICENSE) file for the full license text. 98 | -------------------------------------------------------------------------------- /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": "casbin-prisma-adapter", 3 | "version": "1.7.0", 4 | "description": "Prisma adapter for Casbin", 5 | "main": "lib/adapter.js", 6 | "typings": "lib/adapter.d.ts", 7 | "scripts": { 8 | "build": "rimraf lib && tsc", 9 | "lint": "eslint --ext .ts src/ test/", 10 | "format": "npm run format:check -- --write", 11 | "format:check": "prettier --check \"**/*.{md,json}\" \"{src,test}/**/*.ts\"", 12 | "test": "jest --forceExit" 13 | }, 14 | "devDependencies": { 15 | "@prisma/client": "^6.0.0", 16 | "@semantic-release/git": "^10.0.1", 17 | "@types/jest": "^28.1.6", 18 | "@types/node": "^22.10.1", 19 | "@typescript-eslint/eslint-plugin": "^3.4.0", 20 | "@typescript-eslint/parser": "^3.4.0", 21 | "casbin": "^5.16.0", 22 | "eslint": "^7.3.1", 23 | "eslint-config-prettier": "^6.11.0", 24 | "jest": "^28.1.3", 25 | "prettier": "^2.7.1", 26 | "prisma": "^6.0.0", 27 | "rimraf": "^3.0.2", 28 | "semantic-release": "^19.0.3", 29 | "ts-jest": "^28.0.8", 30 | "ts-node": "^10.9.1", 31 | "typescript": "^5.1.6" 32 | }, 33 | "peerDependencies": { 34 | "@prisma/client": "^6.0.0", 35 | "casbin": "^5.16.0", 36 | "prisma": "^6.0.0" 37 | }, 38 | "files": [ 39 | "lib", 40 | "test", 41 | "examples", 42 | "prisma" 43 | ], 44 | "jest": { 45 | "testURL": "http://localhost", 46 | "transform": { 47 | "^.+\\.(ts|tsx)$": "ts-jest" 48 | }, 49 | "testMatch": [ 50 | "**/test/*.+(ts|tsx)" 51 | ], 52 | "moduleFileExtensions": [ 53 | "ts", 54 | "tsx", 55 | "js", 56 | "jsx", 57 | "json", 58 | "node" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /prisma/casbin_rule.prisma: -------------------------------------------------------------------------------- 1 | model CasbinRule { 2 | id Int @id @default(autoincrement()) 3 | ptype String 4 | v0 String? 5 | v1 String? 6 | v2 String? 7 | v3 String? 8 | v4 String? 9 | v5 String? 10 | 11 | @@map("casbin_rule") 12 | } 13 | -------------------------------------------------------------------------------- /prisma/casbin_rule.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS `casbin`; 2 | 3 | USE casbin; 4 | 5 | CREATE TABLE IF NOT EXISTS `casbin_rule` ( 6 | `id` int NOT NULL AUTO_INCREMENT, 7 | `ptype` varchar(255) DEFAULT NULL, 8 | `v0` varchar(255) DEFAULT NULL, 9 | `v1` varchar(255) DEFAULT NULL, 10 | `v2` varchar(255) DEFAULT NULL, 11 | `v3` varchar(255) DEFAULT NULL, 12 | `v4` varchar(255) DEFAULT NULL, 13 | `v5` varchar(255) DEFAULT NULL, 14 | PRIMARY KEY (`id`) 15 | ); 16 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mysql" 3 | url = "mysql://root@localhost:3306/casbin" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model CasbinRule { 11 | id Int @id @default(autoincrement()) 12 | ptype String 13 | v0 String? 14 | v1 String? 15 | v2 String? 16 | v3 String? 17 | v4 String? 18 | v5 String? 19 | 20 | @@map("casbin_rule") 21 | } 22 | -------------------------------------------------------------------------------- /src/adapter.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter, Model } from 'casbin'; 2 | import type { CasbinRule } from '@prisma/client'; 3 | 4 | import { Helper } from 'casbin'; 5 | import { PrismaClient } from '@prisma/client'; 6 | import { Prisma } from '@prisma/client'; 7 | 8 | export class PrismaAdapter implements Adapter { 9 | #option?: Prisma.PrismaClientOptions; 10 | #prisma: PrismaClient; 11 | 12 | filtered = false; 13 | 14 | public isFiltered(): boolean { 15 | return this.filtered; 16 | } 17 | 18 | public enableFiltered(enabled: boolean): void { 19 | this.filtered = enabled; 20 | } 21 | 22 | /** 23 | * @param option It should be PrismaClientOptions or PrismaClient. 24 | * You should later call open() to activate it. 25 | */ 26 | constructor(option?: Prisma.PrismaClientOptions | PrismaClient) { 27 | if (option instanceof PrismaClient) { 28 | this.#prisma = option; 29 | } else { 30 | this.#option = option; 31 | } 32 | } 33 | 34 | async loadPolicy(model: Model): Promise { 35 | const lines = await this.#prisma.casbinRule.findMany(); 36 | 37 | for (const line of lines) { 38 | this.#loadPolicyLine(line, model); 39 | } 40 | } 41 | 42 | /** 43 | * loadFilteredPolicy loads policy rules that match the filter from the storage; 44 | * use an empty string for selecting all values in a certain field. 45 | */ 46 | async loadFilteredPolicy( 47 | model: Model, 48 | filter: { [key: string]: string[][] } 49 | ): Promise { 50 | const whereFilter = Object.keys(filter) 51 | .map((ptype) => { 52 | const policyPatterns = filter[ptype]; 53 | return policyPatterns.map((policyPattern) => { 54 | return { 55 | ptype, 56 | ...(policyPattern[0] && { v0: policyPattern[0] }), 57 | ...(policyPattern[1] && { v1: policyPattern[1] }), 58 | ...(policyPattern[2] && { v2: policyPattern[2] }), 59 | ...(policyPattern[3] && { v3: policyPattern[3] }), 60 | ...(policyPattern[4] && { v4: policyPattern[4] }), 61 | ...(policyPattern[5] && { v5: policyPattern[5] }), 62 | }; 63 | }); 64 | }) 65 | .flat(); 66 | const lines = await this.#prisma.casbinRule.findMany({ 67 | where: { 68 | OR: whereFilter, 69 | }, 70 | }); 71 | lines.forEach((line) => this.#loadPolicyLine(line, model)); 72 | this.enableFiltered(true); 73 | } 74 | 75 | async savePolicy(model: Model): Promise { 76 | await this.#prisma.$executeRaw`DELETE FROM casbin_rule;`; 77 | 78 | const lines: Prisma.CasbinRuleCreateInput[] = []; 79 | 80 | const savePolicyType = (ptype: string): void => { 81 | const astMap = model.model.get(ptype); 82 | if (astMap) { 83 | for (const [ptype, ast] of astMap) { 84 | for (const rule of ast.policy) { 85 | const line = this.#savePolicyLine(ptype, rule); 86 | lines.push(line); 87 | } 88 | } 89 | } 90 | }; 91 | 92 | savePolicyType('p'); 93 | savePolicyType('g'); 94 | 95 | // https://github.com/prisma/prisma-client-js/issues/332 96 | await this.#prisma.casbinRule.createMany({ data: lines }); 97 | 98 | return true; 99 | } 100 | 101 | async addPolicy(sec: string, ptype: string, rule: string[]): Promise { 102 | const line = this.#savePolicyLine(ptype, rule); 103 | await this.#prisma.casbinRule.create({ data: line }); 104 | } 105 | 106 | async addPolicies( 107 | sec: string, 108 | ptype: string, 109 | rules: string[][] 110 | ): Promise { 111 | const processes: Array> = []; 112 | for (const rule of rules) { 113 | const line = this.#savePolicyLine(ptype, rule); 114 | const p = this.#prisma.casbinRule.create({ data: line }); 115 | processes.push(p); 116 | } 117 | 118 | // https://github.com/prisma/prisma-client-js/issues/332 119 | await Promise.all(processes); 120 | } 121 | 122 | async removePolicy( 123 | sec: string, 124 | ptype: string, 125 | rule: string[] 126 | ): Promise { 127 | const line = this.#savePolicyLine(ptype, rule); 128 | await this.#prisma.casbinRule.deleteMany({ where: line }); 129 | } 130 | 131 | async removePolicies( 132 | sec: string, 133 | ptype: string, 134 | rules: string[][] 135 | ): Promise { 136 | const processes: Array> = []; 137 | for (const rule of rules) { 138 | const line = this.#savePolicyLine(ptype, rule); 139 | const p = this.#prisma.casbinRule.deleteMany({ where: line }); 140 | processes.push(p); 141 | } 142 | 143 | // https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/transactions#bulk-operations 144 | await Promise.all(processes); 145 | } 146 | 147 | async removeFilteredPolicy( 148 | sec: string, 149 | ptype: string, 150 | fieldIndex: number, 151 | ...fieldValues: string[] 152 | ): Promise { 153 | const line: Prisma.CasbinRuleCreateInput = { ptype }; 154 | 155 | const idx = fieldIndex + fieldValues.length; 156 | if (fieldIndex <= 0 && 0 < idx) { 157 | line.v0 = fieldValues[0 - fieldIndex]; 158 | } 159 | if (fieldIndex <= 1 && 1 < idx) { 160 | line.v1 = fieldValues[1 - fieldIndex]; 161 | } 162 | if (fieldIndex <= 2 && 2 < idx) { 163 | line.v2 = fieldValues[2 - fieldIndex]; 164 | } 165 | if (fieldIndex <= 3 && 3 < idx) { 166 | line.v3 = fieldValues[3 - fieldIndex]; 167 | } 168 | if (fieldIndex <= 4 && 4 < idx) { 169 | line.v4 = fieldValues[4 - fieldIndex]; 170 | } 171 | if (fieldIndex <= 5 && 5 < idx) { 172 | line.v5 = fieldValues[5 - fieldIndex]; 173 | } 174 | 175 | await this.#prisma.casbinRule.deleteMany({ where: line }); 176 | } 177 | 178 | async close(): Promise { 179 | return this.#prisma.$disconnect(); 180 | } 181 | 182 | static async newAdapter( 183 | option?: Prisma.PrismaClientOptions | PrismaClient 184 | ): Promise { 185 | const a = new PrismaAdapter(option); 186 | await a.#open(); 187 | 188 | return a; 189 | } 190 | 191 | #open = async (): Promise => { 192 | if (!this.#option) { 193 | this.#option = {}; 194 | } 195 | if (!this.#prisma) { 196 | this.#prisma = new PrismaClient(this.#option); 197 | } 198 | await this.#prisma.$connect(); 199 | }; 200 | 201 | #loadPolicyLine = ( 202 | line: Prisma.CasbinRuleCreateInput, 203 | model: Model 204 | ): void => { 205 | const result = 206 | line.ptype + 207 | ', ' + 208 | [line.v0, line.v1, line.v2, line.v3, line.v4, line.v5] 209 | .filter((n) => n) 210 | .join(', '); 211 | Helper.loadPolicyLine(result, model); 212 | }; 213 | 214 | #savePolicyLine = ( 215 | ptype: string, 216 | rule: string[] 217 | ): Prisma.CasbinRuleCreateInput => { 218 | const line: Prisma.CasbinRuleCreateInput = { ptype }; 219 | 220 | if (rule.length > 0) { 221 | line.v0 = rule[0]; 222 | } 223 | if (rule.length > 1) { 224 | line.v1 = rule[1]; 225 | } 226 | if (rule.length > 2) { 227 | line.v2 = rule[2]; 228 | } 229 | if (rule.length > 3) { 230 | line.v3 = rule[3]; 231 | } 232 | if (rule.length > 4) { 233 | line.v4 = rule[4]; 234 | } 235 | if (rule.length > 5) { 236 | line.v5 = rule[5]; 237 | } 238 | 239 | return line; 240 | }; 241 | } 242 | -------------------------------------------------------------------------------- /test/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 { newEnforcer, Enforcer, Util } from 'casbin'; 16 | import { PrismaAdapter } from '../src/adapter'; 17 | 18 | function array2DEqualsIgnoreOrder(a: string[][], b: string[][]): boolean { 19 | return Util.array2DEquals(a.sort(), b.sort()); 20 | } 21 | 22 | async function testGetPolicy(e: Enforcer, res: string[][]): Promise { 23 | const myRes = await e.getPolicy(); 24 | expect(array2DEqualsIgnoreOrder(res, myRes)).toBe(true); 25 | } 26 | 27 | async function testGetGroupingPolicy( 28 | e: Enforcer, 29 | res: string[][] 30 | ): Promise { 31 | const myRes = await e.getGroupingPolicy(); 32 | expect(array2DEqualsIgnoreOrder(res, myRes)).toBe(true); 33 | } 34 | 35 | test( 36 | 'TestAdapter', 37 | async () => { 38 | const a = await PrismaAdapter.newAdapter(); 39 | 40 | try { 41 | // Because the DB is empty at first, 42 | // so we need to load the policy from the file adapter (.CSV) first. 43 | let e = await newEnforcer( 44 | 'examples/rbac_model.conf', 45 | 'examples/rbac_policy.csv' 46 | ); 47 | 48 | // This is a trick to save the current policy to the DB. 49 | // We can't call e.savePolicy() because the adapter in the enforcer is still the file adapter. 50 | // The current policy means the policy in the Node-Casbin enforcer (aka in memory). 51 | await a.savePolicy(e.getModel()); 52 | 53 | // Clear the current policy. 54 | e.clearPolicy(); 55 | testGetPolicy(e, []); 56 | 57 | // Load the policy from DB. 58 | await a.loadPolicy(e.getModel()); 59 | testGetPolicy(e, [ 60 | ['alice', 'data1', 'read'], 61 | ['bob', 'data2', 'write'], 62 | ['data2_admin', 'data2', 'read'], 63 | ['data2_admin', 'data2', 'write'], 64 | ]); 65 | 66 | // Note: you don't need to look at the above code 67 | // if you already have a working DB with policy inside. 68 | 69 | // Now the DB has policy, so we can provide a normal use case. 70 | // Create an adapter and an enforcer. 71 | // newEnforcer() will load the policy automatically. 72 | e = await newEnforcer('examples/rbac_model.conf', a); 73 | testGetPolicy(e, [ 74 | ['alice', 'data1', 'read'], 75 | ['bob', 'data2', 'write'], 76 | ['data2_admin', 'data2', 'read'], 77 | ['data2_admin', 'data2', 'write'], 78 | ]); 79 | 80 | // Add policy to DB 81 | await a.addPolicy('', 'p', ['role', 'res', 'action']); 82 | e = await newEnforcer('examples/rbac_model.conf', a); 83 | testGetPolicy(e, [ 84 | ['alice', 'data1', 'read'], 85 | ['bob', 'data2', 'write'], 86 | ['data2_admin', 'data2', 'read'], 87 | ['data2_admin', 'data2', 'write'], 88 | ['role', 'res', 'action'], 89 | ]); 90 | 91 | // Add policyList to DB 92 | await a.addPolicies('', 'p', [ 93 | ['role', 'res', 'GET'], 94 | ['role', 'res', 'POST'], 95 | ]); 96 | e = await newEnforcer('examples/rbac_model.conf', a); 97 | testGetPolicy(e, [ 98 | ['alice', 'data1', 'read'], 99 | ['bob', 'data2', 'write'], 100 | ['data2_admin', 'data2', 'read'], 101 | ['data2_admin', 'data2', 'write'], 102 | ['role', 'res', 'action'], 103 | ['role', 'res', 'GET'], 104 | ['role', 'res', 'POST'], 105 | ]); 106 | 107 | // Remove policy from DB 108 | await a.removePolicy('', 'p', ['role', 'res', 'action']); 109 | e = await newEnforcer('examples/rbac_model.conf', a); 110 | testGetPolicy(e, [ 111 | ['alice', 'data1', 'read'], 112 | ['bob', 'data2', 'write'], 113 | ['data2_admin', 'data2', 'read'], 114 | ['data2_admin', 'data2', 'write'], 115 | ['role', 'res', 'GET'], 116 | ['role', 'res', 'POST'], 117 | ]); 118 | 119 | // Remove policyList from DB 120 | await a.removePolicies('', 'p', [ 121 | ['role', 'res', 'GET'], 122 | ['role', 'res', 'POST'], 123 | ]); 124 | e = await newEnforcer('examples/rbac_model.conf', a); 125 | testGetPolicy(e, [ 126 | ['alice', 'data1', 'read'], 127 | ['bob', 'data2', 'write'], 128 | ['data2_admin', 'data2', 'read'], 129 | ['data2_admin', 'data2', 'write'], 130 | ]); 131 | 132 | testGetGroupingPolicy(e, [['alice', 'data2_admin']]); 133 | 134 | // Remove groupingPolicy from DB 135 | await e.deleteUser('alice'); 136 | testGetGroupingPolicy(e, []); 137 | } finally { 138 | await a.close(); 139 | } 140 | }, 141 | 60 * 1000 142 | ); 143 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------