├── .gitignore ├── index.ts ├── tests ├── tsconfig.json ├── assets │ ├── model.conf │ ├── policies.json │ └── roles.json ├── helpers.ts ├── adapter.spec.ts └── enforcer.spec.ts ├── tsconfig.json ├── migrations ├── 1591572942519_pkey-and-uniq.ts └── 1587132340023_initial.ts ├── .github └── workflows │ └── run-tests.yml ├── LICENSE ├── lib ├── model.ts ├── adapter.ts └── repository.ts ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.d.ts 3 | 4 | node_modules/ 5 | coverage/ -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | PostgresAdapaterOptions, 3 | 4 | CasbinFilter, 5 | CasbinRuleFilter 6 | } from "./lib/model"; 7 | 8 | import { PostgresAdapter } from "./lib/adapter"; 9 | export default PostgresAdapter; 10 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": false, 7 | "resolveJsonModule": true 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "declaration": true, 7 | "sourceMap": false 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "tests" 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/assets/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) && globMatch(r.obj, p.obj) && globMatch(r.act, p.act) -------------------------------------------------------------------------------- /migrations/1591572942519_pkey-and-uniq.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate'; 2 | 3 | export async function up(pgm: MigrationBuilder): Promise { 4 | pgm.addConstraint("casbin", "casbin_pkey", { primaryKey: "id" }); 5 | pgm.addConstraint("casbin", "casbin_uniq_rule", { unique: "rule" }); 6 | } 7 | 8 | export async function down(pgm: MigrationBuilder): Promise { 9 | pgm.dropConstraint("casbin", "casbin_uniq_rule"); 10 | pgm.dropConstraint("casbin", "casbin_pkey"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/assets/policies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ptype": "p", 4 | "rule": [ 5 | "user1", 6 | "data1", 7 | "read" 8 | ] 9 | }, 10 | { 11 | "ptype": "p", 12 | "rule": [ 13 | "user1", 14 | "data1", 15 | "write" 16 | ] 17 | }, 18 | { 19 | "ptype": "p", 20 | "rule": [ 21 | "user2", 22 | "data1", 23 | "*" 24 | ] 25 | }, 26 | { 27 | "ptype": "p", 28 | "rule": [ 29 | "user3", 30 | "data2", 31 | "read" 32 | ] 33 | }, 34 | { 35 | "ptype": "p", 36 | "rule": [ 37 | "role:users", 38 | "*", 39 | "read" 40 | ] 41 | }, 42 | { 43 | "ptype": "p", 44 | "rule": [ 45 | "role:admin", 46 | "*", 47 | "*" 48 | ] 49 | } 50 | ] -------------------------------------------------------------------------------- /tests/assets/roles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ptype": "g", 4 | "rule": [ 5 | "user1", 6 | "role:users" 7 | ] 8 | }, 9 | { 10 | "ptype": "g", 11 | "rule": [ 12 | "user10", 13 | "role:users" 14 | ] 15 | }, 16 | { 17 | "ptype": "g", 18 | "rule": [ 19 | "user11", 20 | "role:admin" 21 | ] 22 | }, 23 | { 24 | "ptype": "g", 25 | "rule": [ 26 | "user12", 27 | "role:admin" 28 | ] 29 | }, 30 | { 31 | "ptype": "g", 32 | "rule": [ 33 | "user1", 34 | "role:custom" 35 | ] 36 | }, 37 | { 38 | "ptype": "g", 39 | "rule": [ 40 | "user10", 41 | "role:custom" 42 | ] 43 | }, 44 | { 45 | "ptype": "g", 46 | "rule": [ 47 | "user11", 48 | "role:custom" 49 | ] 50 | } 51 | ] -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x, 18.x] 12 | 13 | services: 14 | postgres: 15 | image: postgres:11-alpine 16 | ports: 17 | - 5432:5432 18 | env: 19 | POSTGRES_USER: casbin 20 | POSTGRES_PASSWORD: casbin 21 | POSTGRES_DB: casbin 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Install dependencies 32 | run: npm ci 33 | 34 | - name: Run unit tests 35 | run: npm run test:ci 36 | env: 37 | CI: true 38 | 39 | - name: Collect coverage 40 | uses: coverallsapp/github-action@master 41 | if: matrix.node-version == '12.x' 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Touchify 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 21 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/model.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-explicit-any: 0 */ 2 | 3 | import { Duplex } from "stream"; 4 | import { ConnectionOptions } from "tls"; 5 | 6 | export type CasbinRuleFilter = Array; 7 | export type CasbinFilter = Record; 8 | 9 | export interface CasbinRule { 10 | ptype: string; 11 | rule: string[]; 12 | } 13 | 14 | export interface PostgresAdapaterOptions { 15 | // Custom options 16 | migrate?: boolean; 17 | dbClient?: (exec: (client: any) => Promise) => Promise; 18 | 19 | // Client Config 20 | user?: string; 21 | database?: string; 22 | password?: string; 23 | port?: number; 24 | host?: string; 25 | connectionString?: string; 26 | keepAlive?: boolean; 27 | stream?: Duplex; 28 | statement_timeout?: false | number; 29 | parseInputDatesAsUTC?: boolean; 30 | ssl?: boolean | ConnectionOptions; 31 | query_timeout?: number; 32 | keepAliveInitialDelayMillis?: number; 33 | idle_in_transaction_session_timeout?: number; 34 | 35 | // Pool Config 36 | poolSize?: number; 37 | poolIdleTimeout?: number; 38 | reapIntervalMillis?: number; 39 | binary?: boolean; 40 | parseInt8?: boolean; 41 | } 42 | -------------------------------------------------------------------------------- /migrations/1587132340023_initial.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from "node-pg-migrate"; 2 | 3 | export function up(pgm: MigrationBuilder): void { 4 | pgm.createTable("casbin", { 5 | id: "serial", 6 | ptype: { type: "text", notNull: true }, 7 | rule: { type: "jsonb", notNull: true } 8 | }); 9 | 10 | pgm.addIndex("casbin", "ptype", { 11 | name: "idx_casbin_ptype", 12 | method: "btree" 13 | }); 14 | 15 | pgm.sql(`CREATE INDEX idx_casbin_rule_v0 ON casbin USING btree ((rule->>0))`); 16 | pgm.sql(`CREATE INDEX idx_casbin_rule_v1 ON casbin USING btree ((rule->>1))`); 17 | pgm.sql(`CREATE INDEX idx_casbin_rule_v2 ON casbin USING btree ((rule->>2))`); 18 | pgm.sql(`CREATE INDEX idx_casbin_rule_v3 ON casbin USING btree ((rule->>3))`); 19 | pgm.sql(`CREATE INDEX idx_casbin_rule_v4 ON casbin USING btree ((rule->>4))`); 20 | pgm.sql(`CREATE INDEX idx_casbin_rule_v5 ON casbin USING btree ((rule->>5))`); 21 | } 22 | 23 | export function down(pgm: MigrationBuilder): void { 24 | pgm.dropIndex("casbin", "ptype", { name: "idx_casbin_ptype" }); 25 | 26 | pgm.sql(`DROP INDEX idx_casbin_rule_v0`); 27 | pgm.sql(`DROP INDEX idx_casbin_rule_v1`); 28 | pgm.sql(`DROP INDEX idx_casbin_rule_v2`); 29 | pgm.sql(`DROP INDEX idx_casbin_rule_v3`); 30 | pgm.sql(`DROP INDEX idx_casbin_rule_v4`); 31 | pgm.sql(`DROP INDEX idx_casbin_rule_v5`); 32 | 33 | pgm.dropTable("casbin"); 34 | } 35 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from "pg"; 2 | 3 | import { 4 | newModel, 5 | newEnforcer, 6 | 7 | Model, 8 | Enforcer, 9 | Assertion 10 | } from "casbin"; 11 | 12 | import PostgresAdapter from ".."; 13 | import { CasbinRule } from "../lib/model"; 14 | 15 | export const connectionString = "postgresql://casbin:casbin@localhost:5432/casbin"; 16 | export const pool = new Pool({ connectionString }); 17 | 18 | //#region New Methods 19 | 20 | export function buildModel(): Model { 21 | const m = newModel(); 22 | m.loadModel(__dirname + "/assets/model.conf"); 23 | 24 | return m; 25 | } 26 | 27 | export function buildAdapter(): Promise { 28 | return PostgresAdapter.newAdapter({ connectionString }); 29 | } 30 | 31 | export async function buildEnforcer(): Promise<[Model, PostgresAdapter, Enforcer]> { 32 | const m = buildModel(); 33 | const a = await buildAdapter(); 34 | const e = await newEnforcer(m, a); 35 | 36 | return [m, a, e]; 37 | } 38 | 39 | //#endregion 40 | 41 | //#region Sample Data 42 | 43 | export function getSamplePolicies(): CasbinRule[] { 44 | return require("./assets/policies.json"); 45 | } 46 | 47 | export function getSampleRoles(): CasbinRule[] { 48 | return require("./assets/roles.json"); 49 | } 50 | 51 | export function getSampleData(): CasbinRule[] { 52 | return getSamplePolicies().concat(getSampleRoles()); 53 | } 54 | 55 | export async function importSampleData(): Promise { 56 | const rules = getSampleData(); 57 | 58 | const req: string[] = []; 59 | const values: string[] = []; 60 | 61 | let i = 1; 62 | for (const { ptype, rule } of rules) { 63 | req.push(`($${i++}, $${i++}::jsonb)`); 64 | values.push(ptype, JSON.stringify(rule)); 65 | } 66 | 67 | await pool.query( 68 | "INSERT INTO casbin (ptype, rule) VALUES " + req.join(", "), 69 | values 70 | ); 71 | } 72 | 73 | //#endregion 74 | 75 | //#region DB Tests 76 | 77 | export async function dbGetAll(): Promise { 78 | const { rows } = await pool.query("SELECT ptype, rule FROM casbin"); 79 | return rows; 80 | } 81 | 82 | export function getRulesFromModel(m: Model, sec: string): CasbinRule[] { 83 | const rules: CasbinRule[] = []; 84 | 85 | let astMap = m.model.get(sec) as Map; 86 | for (const [ptype, ast] of astMap) { 87 | for (const rule of ast.policy) { 88 | rules.push({ ptype, rule }); 89 | } 90 | } 91 | 92 | return rules; 93 | } 94 | 95 | //#endregion 96 | 97 | //#region Clean Methods 98 | 99 | export async function cleanAdapter(a: PostgresAdapter): Promise { 100 | await a.close(); 101 | } 102 | 103 | export async function cleanDB(): Promise { 104 | await pool.query("DELETE FROM casbin"); 105 | } 106 | 107 | export async function cleanEnv(): Promise { 108 | await pool.end(); 109 | } 110 | 111 | //#endregion 112 | -------------------------------------------------------------------------------- /lib/adapter.ts: -------------------------------------------------------------------------------- 1 | import { Adapter, Helper, Model, Assertion } from "casbin"; 2 | 3 | import { CasbinRepository } from "./repository"; 4 | 5 | import { 6 | PostgresAdapaterOptions, 7 | 8 | CasbinRule, 9 | CasbinFilter 10 | } from "./model"; 11 | 12 | export class PostgresAdapter implements Adapter { 13 | private filtered = true; 14 | private readonly repo: CasbinRepository; 15 | 16 | private constructor(options?: PostgresAdapaterOptions) { 17 | this.repo = new CasbinRepository(options); 18 | } 19 | 20 | public static async newAdapter(options?: PostgresAdapaterOptions): Promise { 21 | const adapter = new PostgresAdapter(options); 22 | await adapter.open(); 23 | return adapter; 24 | } 25 | 26 | public static async migrate(options?: PostgresAdapaterOptions): Promise { 27 | const repo = new CasbinRepository(options); 28 | await repo.migrate(); 29 | await repo.close(); 30 | } 31 | 32 | public async open(): Promise { 33 | await this.repo.open(); 34 | } 35 | 36 | public async close(): Promise { 37 | await this.repo.close(); 38 | } 39 | 40 | public isFiltered(): boolean { 41 | return this.filtered; 42 | } 43 | 44 | public enabledFiltered(enabled: boolean): void { 45 | this.filtered = enabled; 46 | } 47 | 48 | public async loadPolicy(model: Model): Promise { 49 | const rules = await this.repo.getAllPolicies(); 50 | loadPolicyLines(model, rules); 51 | } 52 | 53 | public async loadFilteredPolicy(model: Model, filter: CasbinFilter): Promise { 54 | const rules = await this.repo.getFilteredPolicies(filter); 55 | loadPolicyLines(model, rules); 56 | } 57 | 58 | public async savePolicy(model: Model): Promise { 59 | await this.repo.clearPolicies(); 60 | 61 | const rules: CasbinRule[] = []; 62 | 63 | let astMap = model.model.get("p") as Map; 64 | for (const [ptype, ast] of astMap) { 65 | for (const rule of ast.policy) { 66 | rules.push({ ptype, rule }); 67 | } 68 | } 69 | 70 | astMap = model.model.get("g") as Map; 71 | for (const [ptype, ast] of astMap) { 72 | for (const rule of ast.policy) { 73 | rules.push({ ptype, rule }); 74 | } 75 | } 76 | 77 | if (rules.length) { 78 | await this.repo.insertPolicies(rules); 79 | } 80 | 81 | return rules.length > 0; 82 | } 83 | 84 | public addPolicy(sec: string, ptype: string, rule: string[]): Promise { 85 | return this.repo.insertPolicy(ptype, rule); 86 | } 87 | 88 | public removePolicy(sec: string, ptype: string, rule: string[]): Promise { 89 | return this.repo.deletePolicies(ptype, rule); 90 | } 91 | 92 | public removeFilteredPolicy(sec: string, ptype: string, fieldIndex: number, ...fieldValues: string[]): Promise { 93 | return this.repo.deletePolicies(ptype, fieldValues, fieldIndex); 94 | } 95 | } 96 | 97 | function loadPolicyLines(model: Model, rules: CasbinRule[]): void { 98 | rules.forEach(rule => { 99 | Helper.loadPolicyLine(`${rule.ptype}, ${rule.rule.join(", ")}`, model); 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "casbin-pg-adapter", 3 | "version": "1.4.0", 4 | "description": "PostgreSQL native adapter for Node-Casbin with advanced filter capability and improved performance.", 5 | "author": "Touchify ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "types": "index.d.ts", 9 | "homepage": "https://github.com/touchifyapp/casbin-pg-adapter#readme", 10 | "bugs": { 11 | "url": "https://github.com/touchifyapp/casbin-pg-adapter/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/touchifyapp/casbin-pg-adapter.git" 16 | }, 17 | "files": [ 18 | "*.d.ts", 19 | "*.js", 20 | "lib/**/*.d.ts", 21 | "lib/**/*.js", 22 | "migrations/**/*.js" 23 | ], 24 | "scripts": { 25 | "clean": "rimraf *.{js,d.ts} {lib,migrations}/**/*.{js,d.ts}", 26 | "build": "npm run clean && npm run lint && npm run build:ts", 27 | "build:ts": "tsc -p .", 28 | "build:migrations": "tsc migrations/*.ts", 29 | "test": "npm run clean && npm run lint && npm run build:migrations && npm run test:jest", 30 | "test:only": "npm run lint && npm run test:jest", 31 | "test:coverage": "npm run test -- -- --coverage", 32 | "test:jest": "jest --runInBand", 33 | "test:startdb": "docker run --name casbin-pg-adapter-db -e POSTGRES_USER=casbin -e POSTGRES_PASSWORD=casbin -e POSTGRES_DB=casbin -p 5432:5432 -d postgres:11-alpine", 34 | "test:cleandb": "docker rm casbin-pg-adapter-db --force || echo No DB running", 35 | "test:ci": "npm run test:coverage -- --ci", 36 | "lint": "npm run lint:ts", 37 | "lint:ts": "eslint --ext .ts *.ts {lib,migrations}/**/*.ts", 38 | "migrate": "node-pg-migrate", 39 | "preversion": "npm run lint", 40 | "postversion": "git push && git push --tags", 41 | "prepublishOnly": "npm run test && npm run build" 42 | }, 43 | "dependencies": { 44 | "casbin": "^5.0.4", 45 | "node-pg-migrate": "^5.1.0", 46 | "pg": "^8.2.1" 47 | }, 48 | "devDependencies": { 49 | "@types/jest": "^25.2.3", 50 | "@types/node": "^12.0.0", 51 | "@types/pg": "^7.14.3", 52 | "@typescript-eslint/eslint-plugin": "^3.1.0", 53 | "@typescript-eslint/parser": "^3.1.0", 54 | "eslint": "^7.2.0", 55 | "jest": "^26.0.1", 56 | "rimraf": "^3.0.2", 57 | "ts-jest": "^26.1.0", 58 | "typescript": "^3.9.5" 59 | }, 60 | "keywords": [ 61 | "casbin", 62 | "node-casbin", 63 | "adapter", 64 | "postgres", 65 | "pg", 66 | "node-pg", 67 | "access-control", 68 | "authorization", 69 | "auth", 70 | "authz", 71 | "acl", 72 | "rbac", 73 | "abac" 74 | ], 75 | "jest": { 76 | "preset": "ts-jest", 77 | "testEnvironment": "node", 78 | "testMatch": [ 79 | "**/tests/**/*.spec.ts" 80 | ], 81 | "collectCoverageFrom": [ 82 | "*.ts", 83 | "lib/**/*.ts" 84 | ] 85 | }, 86 | "eslintConfig": { 87 | "parser": "@typescript-eslint/parser", 88 | "parserOptions": { 89 | "project": [ 90 | "./tsconfig.json", 91 | "./tests/tsconfig.json" 92 | ] 93 | }, 94 | "env": { 95 | "node": true 96 | }, 97 | "plugins": [ 98 | "@typescript-eslint" 99 | ], 100 | "extends": [ 101 | "eslint:recommended", 102 | "plugin:@typescript-eslint/eslint-recommended", 103 | "plugin:@typescript-eslint/recommended" 104 | ], 105 | "rules": { 106 | "@typescript-eslint/no-use-before-define": [ 107 | "error", 108 | { 109 | "functions": false, 110 | "classes": false, 111 | "typedefs": false 112 | } 113 | ], 114 | "@typescript-eslint/explicit-function-return-type": [ 115 | "error", 116 | { 117 | "allowExpressions": true 118 | } 119 | ], 120 | "@typescript-eslint/array-type": [ 121 | "error", 122 | { 123 | "default": "array-simple", 124 | "readonly": "array-simple" 125 | } 126 | ] 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL Casbin Adapter 2 | 3 | [![NPM version](https://img.shields.io/npm/v/casbin-pg-adapter.svg?style=flat-square)](https://npmjs.org/package/casbin-pg-adapter) 4 | [![NPM download](https://img.shields.io/npm/dm/casbin-pg-adapter.svg?style=flat-square)](https://npmjs.org/package/casbin-pg-adapter) 5 | ![Run Tests](https://github.com/touchifyapp/casbin-pg-adapter/workflows/Run%20Tests/badge.svg?branch=master&event=push) 6 | [![Coverage Status](https://coveralls.io/repos/github/touchifyapp/casbin-pg-adapter/badge.svg?branch=master)](https://coveralls.io/github/touchifyapp/casbin-pg-adapter?branch=master) 7 | 8 | [PostgreSQL](https://www.postgresql.org/) native adapter for [Node-Casbin](https://github.com/casbin/node-casbin). With this library, Node-Casbin can load policy from PosgreSQL database or save policy to it. It supports loading filtered policies and is built for improving performances in PostgreSQL. It uses [node-postgres](https://node-postgres.com/) to connect to PostgreSQL. 9 | 10 | `casbin-pg-adapter` also adds advanced filtering capability. You can filter using `LIKE` or `regexp` expressions when using `loadFilteredPolicy`. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm install casbin-pg-adapter 16 | ``` 17 | 18 | ## Simple example 19 | 20 | ```typescript 21 | import { newEnforcer } from "casbin"; 22 | import PostgresAdapter from "casbin-pg-adapter"; 23 | 24 | async function myFunction() { 25 | // Initialize a Postgres adapter and use it in a Node-Casbin enforcer: 26 | // The adapter can not automatically create database. 27 | // But the adapter will automatically and use the table named "casbin". 28 | // I think ORM should not automatically create databases. 29 | const a = await PostgresAdapter.newAdapter({ 30 | connectionString: "postgresql://casbin:casbin@localhost:5432/casbin" 31 | }); 32 | 33 | const e = await newEnforcer("examples/rbac_model.conf", a); 34 | 35 | // Load the policy from DB. 36 | await e.loadPolicy(); 37 | 38 | // Check the permission. 39 | e.enforce("alice", "data1", "read"); 40 | 41 | // Modify the policy. 42 | // await e.addPolicy(...); 43 | // await e.removePolicy(...); 44 | 45 | // Save the policy back to DB. 46 | await e.savePolicy(); 47 | } 48 | ``` 49 | 50 | ## Filtering example 51 | 52 | ```typescript 53 | import { newEnforcer } from "casbin"; 54 | import PostgresAdapter from "casbin-pg-adapter"; 55 | 56 | async function myFunction() { 57 | const a = await PostgresAdapter.newAdapter({ 58 | connectionString: "postgresql://casbin:casbin@localhost:5432/casbin" 59 | }); 60 | 61 | const e = await newEnforcer("examples/rbac_model.conf", a); 62 | 63 | // Load the filtered policy from DB. 64 | await e.loadFilteredPolicy({ 65 | p: ["alice"], 66 | g: ["", "role:admin"] 67 | }); 68 | 69 | // Check the permission. 70 | e.enforce("alice", "data1", "read"); 71 | } 72 | ``` 73 | 74 | ## Advanced filtering example 75 | 76 | ```typescript 77 | import { newEnforcer } from "casbin"; 78 | import PostgresAdapter from "casbin-pg-adapter"; 79 | 80 | async function myFunction() { 81 | const a = await PostgresAdapter.newAdapter({ 82 | connectionString: "postgresql://casbin:casbin@localhost:5432/casbin" 83 | }); 84 | 85 | const e = await newEnforcer("examples/rbac_model.conf", a); 86 | 87 | // Load the filtered policy from DB. 88 | await e.loadFilteredPolicy({ 89 | p: ["regex:(role:.*)|(alice)"], 90 | g: ["", "like:role:%"] 91 | }); 92 | 93 | // Check the permission. 94 | e.enforce("alice", "data1", "read"); 95 | } 96 | ``` 97 | 98 | ## Configuration 99 | 100 | You can pass any [node-postgres](https://node-postgres.com/) options to the Adapter. 101 | See `node-postgres` documentation: [Connecting to PostgreSQL](https://node-postgres.com/features/connecting#Programmatic). 102 | 103 | ## Additional configurations 104 | 105 | #### Avoid database migration 106 | 107 | Additionnally, you can pass the following option to the Adapter: 108 | * `migrate` (*Boolean*): If set to `false`, the Adapter will not apply migration when starting. 109 | 110 | **Note:** If you use this parameter, you should apply migration manually: 111 | 112 | ```typescript 113 | async function startup(): Promise { 114 | PostgresAdapter.migrate({ 115 | connectionString: "postgresql://casbin:casbin@localhost:5432/casbin" 116 | }); 117 | } 118 | 119 | async function createEnforcer(): Promise { 120 | const a = await PostgresAdapter.newAdapter({ 121 | connectionString: "postgresql://casbin:casbin@localhost:5432/casbin", 122 | migrate: false 123 | }); 124 | 125 | return newEnforcer("examples/rbac_model.conf", a); 126 | } 127 | ``` 128 | 129 | #### Disabling filtered behavior 130 | 131 | If you want to use the `savePolicy` feature from `node-casbin`, you have to disable the filtered behavior of `PostgresAdapter`. 132 | You can do it by calling `enableFiltered` on the adapter: 133 | 134 | ```typescript 135 | a.enableFiltered(false); 136 | ``` 137 | 138 | ## Getting Help 139 | 140 | - [Node-Casbin](https://github.com/casbin/node-casbin) 141 | - [Casbin-PG-Adapter](https://github.com/touchifyapp/casbin-pg-adapter) 142 | 143 | ## License 144 | 145 | This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text. 146 | 147 | -------------------------------------------------------------------------------- /lib/repository.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { Pool, PoolClient } from "pg"; 4 | import migrate from "node-pg-migrate"; 5 | 6 | import { 7 | CasbinRule, 8 | 9 | CasbinFilter, 10 | CasbinRuleFilter, 11 | 12 | PostgresAdapaterOptions 13 | } from "./model"; 14 | 15 | export class CasbinRepository { 16 | private readonly options: PostgresAdapaterOptions; 17 | private readonly db: Pool | undefined; 18 | private readonly dbClient: (cb: (client: PoolClient) => Promise) => Promise; 19 | 20 | public constructor(options: PostgresAdapaterOptions = {}) { 21 | this.options = options; 22 | if (options.dbClient) { 23 | this.dbClient = options.dbClient; 24 | } 25 | else { 26 | this.db = new Pool(options); 27 | this.dbClient = buildDbClientFactory(this.db); 28 | } 29 | } 30 | 31 | public async getAllPolicies(): Promise { 32 | return this.dbClient(async (client) => { 33 | const { rows } = await client.query("SELECT ptype, rule FROM casbin"); 34 | return rows; 35 | }); 36 | } 37 | 38 | public async getFilteredPolicies(filter: CasbinFilter): Promise { 39 | const [where, values] = buildWhereClause(filter); 40 | 41 | return this.dbClient(async (client) => { 42 | const { rows } = await client.query("SELECT ptype, rule FROM casbin" + where, values); 43 | return rows; 44 | }); 45 | } 46 | 47 | public async insertPolicy(ptype: string, rule: string[]): Promise { 48 | return this.dbClient(async (client) => { 49 | await client.query( 50 | "INSERT INTO casbin (ptype, rule) VALUES ($1, $2::jsonb)", 51 | [ptype, JSON.stringify(rule)] 52 | ); 53 | }); 54 | } 55 | 56 | public async insertPolicies(rules: CasbinRule[]): Promise { 57 | const req: string[] = []; 58 | const values: string[] = []; 59 | 60 | let i = 1; 61 | for (const { ptype, rule } of rules) { 62 | req.push(`($${i++}, $${i++}::jsonb)`); 63 | values.push(ptype, JSON.stringify(rule)); 64 | } 65 | 66 | return this.dbClient(async (client) => { 67 | await client.query( 68 | "INSERT INTO casbin (ptype, rule) VALUES " + req.join(", "), 69 | values 70 | ); 71 | }); 72 | } 73 | 74 | public async deletePolicies(ptype: string, ruleFilter: CasbinRuleFilter, fieldIndex?: number): Promise { 75 | const values = [ptype]; 76 | const req = `DELETE FROM casbin WHERE ptype=$${values.length} AND ` + buildRuleWhereClause(ruleFilter, values, fieldIndex); 77 | 78 | return this.dbClient(async (client) => { 79 | await client.query(req, values); 80 | }); 81 | } 82 | 83 | public async clearPolicies(): Promise { 84 | return this.dbClient(async (client) => { 85 | await client.query("DELETE FROM casbin"); 86 | }); 87 | } 88 | 89 | public async open(): Promise { 90 | if (this.options.migrate !== false) { 91 | await this.migrate(); 92 | } 93 | } 94 | 95 | public async migrate(): Promise { 96 | return this.dbClient(async (client) => { 97 | await migrate({ 98 | dbClient: client, 99 | direction: "up", 100 | count: Infinity, 101 | migrationsTable: "casbin_migrations", 102 | dir: path.join(__dirname, "..", "migrations"), 103 | ignorePattern: "(.*\\.ts)|(\\..*)", 104 | log: () => void 0 105 | }); 106 | 107 | }); 108 | } 109 | 110 | public async close(): Promise { 111 | if (this.db) { 112 | await this.db.end(); 113 | } 114 | } 115 | } 116 | 117 | //#region Private Functions 118 | 119 | function buildDbClientFactory(pool: Pool): (cb: (client: PoolClient) => Promise) => Promise { 120 | return async function batch(exec: (client: PoolClient) => Promise): Promise { 121 | const client = await pool.connect(); 122 | try { 123 | const res = await Promise.resolve(exec(client)); 124 | client.release(); 125 | return res; 126 | } 127 | catch (err) { 128 | client.release(); 129 | throw err; 130 | } 131 | } 132 | } 133 | 134 | function buildWhereClause(filter: CasbinFilter): [string, string[]] { 135 | if (!filter) { 136 | return ["", []]; 137 | } 138 | 139 | const values: string[] = []; 140 | const res: string[] = []; 141 | 142 | Object.keys(filter).forEach(ptype => { 143 | values.push(ptype); 144 | let typePredicate = `ptype = $${values.length}`; 145 | 146 | if (filter[ptype] && filter[ptype].length) { 147 | const rulePredicate = buildRuleWhereClause(filter[ptype], values); 148 | if (rulePredicate) { 149 | typePredicate = `(${typePredicate} AND (${rulePredicate}))` 150 | } 151 | } 152 | 153 | res.push(typePredicate); 154 | }); 155 | 156 | return [ 157 | res.length ? " WHERE " + res.join(" OR ") : "", 158 | values 159 | ]; 160 | } 161 | 162 | function buildRuleWhereClause(ruleFilter: CasbinRuleFilter, values: string[], fieldIndex = 0): string { 163 | const res: string[] = []; 164 | 165 | ruleFilter.forEach((value, i) => { 166 | if (value === null || value === "" || typeof value === "undefined") return; 167 | 168 | if (value.startsWith("regex:")) { 169 | values.push(value.replace("regex:", "")); 170 | res.push(`rule->>${i + fieldIndex} ~ $${values.length}`); 171 | } 172 | else if (value.startsWith("like:")) { 173 | values.push(value.replace("like:", "")); 174 | res.push(`rule->>${i + fieldIndex} ~~ $${values.length}`); 175 | } 176 | else { 177 | values.push(value); 178 | res.push(`rule->>${i + fieldIndex} = $${values.length}`); 179 | } 180 | }); 181 | 182 | return res.join(" AND "); 183 | } 184 | 185 | //#endregion 186 | -------------------------------------------------------------------------------- /tests/adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import PostgresAdapter from "../"; 2 | import { CasbinRepository } from "../lib/repository"; 3 | 4 | import { 5 | Model 6 | } from "casbin"; 7 | 8 | import { 9 | connectionString, 10 | 11 | buildAdapter, 12 | buildModel, 13 | 14 | cleanDB, 15 | cleanAdapter, 16 | cleanEnv, 17 | 18 | importSampleData, 19 | getSamplePolicies, 20 | getSampleRoles, 21 | 22 | dbGetAll, 23 | getRulesFromModel 24 | } from "./helpers"; 25 | 26 | const 27 | POLICIES = getSamplePolicies(), 28 | ROLES = getSampleRoles(); 29 | 30 | afterAll(cleanEnv); 31 | 32 | describe("PostgresAdapter", () => { 33 | let a: PostgresAdapter; 34 | 35 | describe("#loadPolicy()", () => { 36 | let m: Model; 37 | 38 | beforeAll(async () => { 39 | a = await buildAdapter(); 40 | await importSampleData(); 41 | }); 42 | afterAll(() => Promise.all([ 43 | cleanDB(), 44 | cleanAdapter(a) 45 | ])); 46 | 47 | beforeEach(() => { m = buildModel(); }); 48 | afterEach(() => { m.clearPolicy(); }); 49 | 50 | test("should resolves with void", async () => { 51 | const res = await a.loadPolicy(m); 52 | expect(res).toBeUndefined(); 53 | }); 54 | 55 | test("should fetch policies from DB into Model", async () => { 56 | await a.loadPolicy(m); 57 | 58 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES); 59 | expect(getRulesFromModel(m, "g")).toEqual(ROLES); 60 | }); 61 | }); 62 | 63 | describe("#loadFilteredPolicy()", () => { 64 | let m: Model; 65 | 66 | beforeAll(async () => { 67 | a = await buildAdapter(); 68 | await importSampleData(); 69 | }); 70 | afterAll(() => Promise.all([ 71 | cleanDB(), 72 | cleanAdapter(a) 73 | ])); 74 | 75 | beforeEach(() => { m = buildModel(); }); 76 | afterEach(() => { m.clearPolicy(); }); 77 | 78 | test("should resolves with void", async () => { 79 | const res = await a.loadFilteredPolicy(m, { p: ["", "data1"], g: ["", "role:admin"] }); 80 | expect(res).toBeUndefined(); 81 | }); 82 | 83 | test("should fetch filtered policies from DB into Model", async () => { 84 | await a.loadFilteredPolicy(m, { p: ["", "data1"], g: ["", "role:admin"] }); 85 | 86 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 87 | expect(getRulesFromModel(m, "g")).toEqual(ROLES.filter(p => p.rule[1] === "role:admin")); 88 | }); 89 | 90 | test("should allow LIKE expressions to filter policies from DB into Model", async () => { 91 | await a.loadFilteredPolicy(m, { p: ["like:role:%"], g: ["", "like:role:%"] }); 92 | 93 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES.filter(p => p.rule[0].startsWith("role:"))); 94 | expect(getRulesFromModel(m, "g")).toEqual(ROLES.filter(p => p.rule[1].startsWith("role:"))); 95 | }); 96 | 97 | test("should allow regex expressions to filter policies from DB into Model", async () => { 98 | await a.loadFilteredPolicy(m, { p: ["regex:(role:.*)|(user1)"], g: ["", "regex:role:.*"] }); 99 | 100 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES.filter(p => p.rule[0].startsWith("role:") || p.rule[0] === "user1")); 101 | expect(getRulesFromModel(m, "g")).toEqual(ROLES.filter(p => p.rule[1].startsWith("role:"))); 102 | }); 103 | 104 | test("should load all policies if no filter is passed", async () => { 105 | await a.loadFilteredPolicy(m, null as any); 106 | 107 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES); 108 | expect(getRulesFromModel(m, "g")).toEqual(ROLES); 109 | }); 110 | 111 | test("should load all policies if empty filter is passed", async () => { 112 | await a.loadFilteredPolicy(m, {}); 113 | 114 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES); 115 | expect(getRulesFromModel(m, "g")).toEqual(ROLES); 116 | }); 117 | 118 | test("should take all policies with ptype filter = null", async () => { 119 | await a.loadFilteredPolicy(m, { 120 | p: ["", "data1"], 121 | g: null as any 122 | }); 123 | 124 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 125 | expect(getRulesFromModel(m, "g")).toEqual(ROLES); 126 | }); 127 | 128 | test("should take all policies with ptype filter = empty array", async () => { 129 | await a.loadFilteredPolicy(m, { 130 | p: ["", "data1"], 131 | g: [] 132 | }); 133 | 134 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 135 | expect(getRulesFromModel(m, "g")).toEqual(ROLES); 136 | }); 137 | 138 | test("should take all policies with ptype filter = array with only empty values", async () => { 139 | await a.loadFilteredPolicy(m, { 140 | p: ["", "data1"], 141 | g: ["", ""] 142 | }); 143 | 144 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 145 | expect(getRulesFromModel(m, "g")).toEqual(ROLES); 146 | }); 147 | 148 | test("should not take policies with ptype not specified in filter", async () => { 149 | await a.loadFilteredPolicy(m, { 150 | p: ["", "data1"] 151 | }); 152 | 153 | expect(getRulesFromModel(m, "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 154 | expect(getRulesFromModel(m, "g")).toEqual([]); 155 | }); 156 | }); 157 | 158 | describe("#addPolicy()", () => { 159 | beforeEach(async () => { a = await buildAdapter(); }); 160 | afterEach(() => Promise.all([cleanDB(), cleanAdapter(a)])); 161 | 162 | test("should resolves with void", async () => { 163 | const res = await a.addPolicy("p", "p", ["alice", "data5", "read"]); 164 | expect(res).toBeUndefined(); 165 | }); 166 | 167 | test("should add policy in DB", async () => { 168 | await a.addPolicy("p", "p", ["alice", "data5", "read"]); 169 | const rules = await dbGetAll(); 170 | expect(rules).toEqual([{ ptype: "p", rule: ["alice", "data5", "read"] }]); 171 | }); 172 | 173 | test("should throw when inserting conflicting policy", async () => { 174 | await a.addPolicy("p", "p", ["alice", "data6", "read"]); 175 | await expect(a.addPolicy("p", "p", ["alice", "data6", "read"])) 176 | .rejects.toMatchObject({ 177 | code: "23505", // unique violation 178 | constraint: "casbin_uniq_rule" 179 | }); 180 | }); 181 | }); 182 | 183 | describe("#removePolicy()", () => { 184 | beforeAll(async () => { 185 | a = await buildAdapter(); 186 | await importSampleData(); 187 | }); 188 | afterAll(() => Promise.all([ 189 | cleanDB(), 190 | cleanAdapter(a) 191 | ])); 192 | 193 | test("should resolves with void", async () => { 194 | const res = await a.removePolicy("p", "p", POLICIES[0].rule); 195 | expect(res).toBeUndefined(); 196 | }); 197 | 198 | test("should remove policy from DB", async () => { 199 | await a.removePolicy("p", "p", POLICIES[1].rule); 200 | const rules = await dbGetAll(); 201 | expect(rules).not.toContainEqual(POLICIES[1]); 202 | }); 203 | }); 204 | 205 | describe("#removeFilteredPolicy()", () => { 206 | beforeAll(async () => { 207 | a = await buildAdapter(); 208 | await importSampleData(); 209 | }); 210 | afterAll(() => Promise.all([ 211 | cleanDB(), 212 | cleanAdapter(a) 213 | ])); 214 | 215 | test("should resolves with void", async () => { 216 | const res = await a.removeFilteredPolicy("p", "p", 0, "user2"); 217 | expect(res).toBeUndefined(); 218 | }); 219 | 220 | test("should remove policies from DB", async () => { 221 | await a.removeFilteredPolicy("p", "p", 0, "user1"); 222 | 223 | const rules = await dbGetAll(); 224 | expect(rules).toEqual( 225 | expect.not.arrayContaining( 226 | POLICIES.filter(r => r.rule[0] === "user1") 227 | ) 228 | ); 229 | }); 230 | }); 231 | 232 | describe("#savePolicy()", () => { 233 | let m: Model; 234 | 235 | beforeEach(async () => { 236 | a = await buildAdapter(); 237 | m = await buildModel(); 238 | }); 239 | afterEach(() => Promise.all([ 240 | cleanDB(), 241 | cleanAdapter(a), 242 | m.clearPolicy() 243 | ])); 244 | 245 | test("should resolves with true", async () => { 246 | m.addPolicy("p", "p", ["alice", "data", "read"]); 247 | const res = await a.savePolicy(m); 248 | expect(res).toBe(true); 249 | }); 250 | 251 | test("should resolves with false if no changes to be submited", async () => { 252 | const res = await a.savePolicy(m); 253 | expect(res).toBe(false); 254 | }); 255 | 256 | test("should add policies in DB", async () => { 257 | m.addPolicy("p", "p", ["alice", "data", "read"]); 258 | m.addPolicy("p", "p", ["bob", "data", "write"]); 259 | m.addPolicy("p", "p", ["john", "data", "delete"]); 260 | m.addPolicy("g", "g", ["john", "role:admin"]); 261 | 262 | await a.savePolicy(m); 263 | 264 | const rules = await dbGetAll(); 265 | expect(rules).toEqual( 266 | expect.arrayContaining([ 267 | { ptype: "p", rule: ["alice", "data", "read"] }, 268 | { ptype: "p", rule: ["bob", "data", "write"] }, 269 | { ptype: "p", rule: ["john", "data", "delete"] }, 270 | { ptype: "g", rule: ["john", "role:admin"] } 271 | ]) 272 | ); 273 | }); 274 | 275 | test("should clear database before syncing", async () => { 276 | await importSampleData(); 277 | 278 | m.addPolicy("p", "p", ["alice", "data", "read"]); 279 | m.addPolicy("p", "p", ["bob", "data", "write"]); 280 | m.addPolicy("p", "p", ["john", "data", "delete"]); 281 | 282 | await a.savePolicy(m); 283 | 284 | const rules = await dbGetAll(); 285 | expect(rules).toEqual( 286 | expect.not.arrayContaining(POLICIES) 287 | ); 288 | }); 289 | }); 290 | 291 | describe(".newAdapater()", () => { 292 | let a: PostgresAdapter; 293 | let spy: jest.SpyInstance; 294 | beforeEach(() => { spy = jest.spyOn(CasbinRepository.prototype, "migrate"); }); 295 | afterEach(async () => { spy.mockRestore(); await a.close(); }); 296 | 297 | test("should resolves with PostgresAdapter", async () => { 298 | a = await PostgresAdapter.newAdapter({ connectionString }); 299 | expect(a).toBeInstanceOf(PostgresAdapter); 300 | }); 301 | 302 | test("should call repo.migrate() if not migrate option is passed", async () => { 303 | a = await PostgresAdapter.newAdapter({ connectionString }); 304 | expect(spy).toBeCalledTimes(1); 305 | }); 306 | 307 | test("should not call repo.migrate() if migrate = false", async () => { 308 | a = await PostgresAdapter.newAdapter({ connectionString, migrate: false }); 309 | expect(spy).not.toBeCalled(); 310 | }); 311 | }); 312 | 313 | describe(".migrate()", () => { 314 | let spy: jest.SpyInstance; 315 | beforeEach(() => { spy = jest.spyOn(CasbinRepository.prototype, "migrate"); }); 316 | afterEach(() => { spy.mockRestore(); }); 317 | 318 | test("should resolves with void", async () => { 319 | const res = await PostgresAdapter.migrate({ connectionString }); 320 | expect(res).toBeUndefined(); 321 | }); 322 | 323 | test("should call repo.migrate()", async () => { 324 | await PostgresAdapter.migrate({ connectionString }); 325 | expect(spy).toBeCalledTimes(1); 326 | }); 327 | }); 328 | 329 | }); -------------------------------------------------------------------------------- /tests/enforcer.spec.ts: -------------------------------------------------------------------------------- 1 | import PostgresAdapter from "../"; 2 | 3 | import { 4 | Enforcer, 5 | Model 6 | } from "casbin"; 7 | 8 | import { 9 | buildEnforcer, 10 | buildModel, 11 | 12 | cleanDB, 13 | cleanAdapter, 14 | cleanEnv, 15 | 16 | importSampleData, 17 | getSamplePolicies, 18 | getSampleRoles, 19 | 20 | dbGetAll, 21 | getRulesFromModel 22 | } from "./helpers"; 23 | 24 | const 25 | POLICIES = getSamplePolicies(), 26 | ROLES = getSampleRoles(); 27 | 28 | afterAll(cleanEnv); 29 | 30 | describe("PostgresAdapter", () => { 31 | let m: Model; 32 | let a: PostgresAdapter; 33 | let e: Enforcer; 34 | 35 | describe(".loadPolicy()", () => { 36 | beforeAll(async () => { 37 | [m, a, e] = await buildEnforcer(); 38 | await importSampleData(); 39 | }); 40 | afterAll(async () => { 41 | await cleanAdapter(a); 42 | await cleanDB(); 43 | }); 44 | 45 | beforeEach(() => { m = buildModel(); }); 46 | afterEach(() => { m.clearPolicy(); }); 47 | 48 | test("should resolves with void", async () => { 49 | const res = await e.loadPolicy(); 50 | expect(res).toBeUndefined(); 51 | }); 52 | 53 | test("should fetch policies from DB into Model", async () => { 54 | await e.loadPolicy(); 55 | 56 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES); 57 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES); 58 | }); 59 | }); 60 | 61 | describe(".loadFilteredPolicy()", () => { 62 | beforeAll(async () => { 63 | [m, a, e] = await buildEnforcer(); 64 | await importSampleData(); 65 | }); 66 | afterAll(() => Promise.all([ 67 | cleanDB(), 68 | cleanAdapter(a) 69 | ])); 70 | 71 | beforeEach(() => { m = buildModel(); }); 72 | afterEach(() => { m.clearPolicy(); }); 73 | 74 | test("should resolves with true", async () => { 75 | const res = await e.loadFilteredPolicy({ p: ["", "data1"], g: ["", "role:admin"] }); 76 | expect(res).toBe(true); 77 | }); 78 | 79 | test("should fetch filtered policies from DB into Model", async () => { 80 | await e.loadFilteredPolicy({ p: ["", "data1"], g: ["", "role:admin"] }); 81 | 82 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 83 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES.filter(p => p.rule[1] === "role:admin")); 84 | }); 85 | 86 | test("should allow LIKE expressions to filter policies from DB into Model", async () => { 87 | await e.loadFilteredPolicy({ p: ["like:role:%"], g: ["", "like:role:%"] }); 88 | 89 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES.filter(p => p.rule[0].startsWith("role:"))); 90 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES.filter(p => p.rule[1].startsWith("role:"))); 91 | }); 92 | 93 | test("should allow regex expressions to filter policies from DB into Model", async () => { 94 | await e.loadFilteredPolicy({ p: ["regex:(role:.*)|(user1)"], g: ["", "regex:role:.*"] }); 95 | 96 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES.filter(p => p.rule[0].startsWith("role:") || p.rule[0] === "user1")); 97 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES.filter(p => p.rule[1].startsWith("role:"))); 98 | }); 99 | 100 | test("should load all policies if no filter is passed", async () => { 101 | await e.loadFilteredPolicy(null as any); 102 | 103 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES); 104 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES); 105 | }); 106 | 107 | test("should load all policies if empty filter is passed", async () => { 108 | await e.loadFilteredPolicy({} as any); 109 | 110 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES); 111 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES); 112 | }); 113 | 114 | test("should take all policies with ptype filter = null", async () => { 115 | await e.loadFilteredPolicy({ 116 | p: ["", "data1"], 117 | g: null as any 118 | }); 119 | 120 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 121 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES); 122 | }); 123 | 124 | test("should take all policies with ptype filter = empty array", async () => { 125 | await e.loadFilteredPolicy({ 126 | p: ["", "data1"], 127 | g: [] 128 | }); 129 | 130 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 131 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES); 132 | }); 133 | 134 | test("should take all policies with ptype filter = array with only empty values", async () => { 135 | await e.loadFilteredPolicy({ 136 | p: ["", "data1"], 137 | g: ["", ""] 138 | }); 139 | 140 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 141 | expect(getRulesFromModel(e.getModel(), "g")).toEqual(ROLES); 142 | }); 143 | 144 | test("should not take policies with ptype not specified in filter", async () => { 145 | await e.loadFilteredPolicy({ 146 | p: ["", "data1"] 147 | } as any); 148 | 149 | expect(getRulesFromModel(e.getModel(), "p")).toEqual(POLICIES.filter(p => p.rule[1] === "data1")); 150 | expect(getRulesFromModel(e.getModel(), "g")).toEqual([]); 151 | }); 152 | }); 153 | 154 | describe(".addPolicy()", () => { 155 | beforeAll(async () => { 156 | [m, a, e] = await buildEnforcer(); 157 | }); 158 | afterAll(async () => { 159 | await cleanAdapter(a); 160 | await cleanDB(); 161 | }); 162 | 163 | test("should resolves with true", async () => { 164 | const res = await e.addPolicy("alice", "data1", "read"); 165 | expect(res).toBe(true); 166 | }); 167 | 168 | test("should not add into DB if autoSave is disabled", async () => { 169 | e.enableAutoSave(false); 170 | 171 | await e.addPolicy("alice", "data2", "read"); 172 | 173 | const rules = await dbGetAll(); 174 | expect(rules).not.toContainEqual({ ptype: "p", rule: ["alice", "data2", "read"] }); 175 | }); 176 | 177 | test("should add into DB if autoSave is enabled", async () => { 178 | e.enableAutoSave(true); 179 | 180 | await e.addPolicy("alice", "data3", "read"); 181 | 182 | const rules = await dbGetAll(); 183 | expect(rules).toContainEqual({ ptype: "p", rule: ["alice", "data3", "read"] }); 184 | }); 185 | 186 | test("should return false when entity is not inserted", async () => { 187 | e.enableAutoSave(true); 188 | 189 | const ok = await e.addPolicy("alice", "data4", "read"); 190 | expect(ok).toBe(true); 191 | 192 | const nok = await e.addPolicy("alice", "data4", "read"); 193 | expect(nok).toBe(false); 194 | }); 195 | 196 | test("should throws if duplicate rule is inserted", async () => { 197 | e.enableAutoSave(true); 198 | 199 | await e.addPolicy("alice", "data5", "read"); 200 | 201 | e.clearPolicy(); 202 | 203 | await expect(e.addPolicy("alice", "data5", "read")) 204 | .rejects.toMatchObject({ 205 | code: "23505", // unique violation 206 | constraint: "casbin_uniq_rule" 207 | }); 208 | }); 209 | }); 210 | 211 | describe(".removePolicy()", () => { 212 | beforeAll(async () => { 213 | [m, a, e] = await buildEnforcer(); 214 | await importSampleData(); 215 | await e.loadPolicy(); 216 | }); 217 | afterAll(async () => { 218 | await cleanAdapter(a); 219 | await cleanDB(); 220 | }); 221 | 222 | test("should resolves with true", async () => { 223 | const res = await e.removePolicy(...POLICIES[0].rule); 224 | expect(res).toBe(true); 225 | }); 226 | 227 | test("should not remove from DB if autoSave is disabled", async () => { 228 | e.enableAutoSave(false); 229 | 230 | await e.removePolicy(...POLICIES[1].rule); 231 | 232 | const rules = await dbGetAll(); 233 | expect(rules).toContainEqual(POLICIES[1]); 234 | }); 235 | 236 | test("should remove from DB if autoSave is enabled", async () => { 237 | e.enableAutoSave(true); 238 | 239 | await e.removePolicy(...POLICIES[2].rule); 240 | 241 | const rules = await dbGetAll(); 242 | expect(rules).not.toContainEqual(POLICIES[2]); 243 | }); 244 | 245 | }); 246 | 247 | describe(".removeFilteredPolicy()", () => { 248 | beforeAll(async () => { 249 | [m, a, e] = await buildEnforcer(); 250 | await importSampleData(); 251 | await e.loadPolicy(); 252 | }); 253 | afterAll(async () => { 254 | await cleanAdapter(a); 255 | await cleanDB(); 256 | }); 257 | 258 | test("should resolves with true", async () => { 259 | const res = await e.removeFilteredPolicy(1, POLICIES[3].rule[1]); 260 | expect(res).toBe(true); 261 | }); 262 | 263 | test("should not remove from DB if autoSave is disabled", async () => { 264 | e.enableAutoSave(false); 265 | 266 | await e.removeFilteredPolicy(1, POLICIES[4].rule[1]); 267 | 268 | const rules = await dbGetAll(); 269 | expect(rules).toEqual( 270 | expect.arrayContaining(POLICIES.filter(p => p.rule[1] === POLICIES[4].rule[1])) 271 | ); 272 | }); 273 | 274 | test("should remove from DB if autoSave is enabled", async () => { 275 | e.enableAutoSave(true); 276 | 277 | await e.removeFilteredPolicy(1, POLICIES[0].rule[1]); 278 | 279 | const rules = await dbGetAll(); 280 | expect(rules).toEqual( 281 | expect.not.arrayContaining(POLICIES.filter(p => p.rule[1] === POLICIES[0].rule[1])) 282 | ); 283 | }); 284 | 285 | }); 286 | 287 | describe(".addGroupingPolicy()", () => { 288 | beforeAll(async () => { 289 | [m, a, e] = await buildEnforcer(); 290 | }); 291 | afterAll(async () => { 292 | await cleanAdapter(a); 293 | await cleanDB(); 294 | }); 295 | 296 | test("should resolves with true", async () => { 297 | const res = await e.addGroupingPolicy("role1", "user1"); 298 | expect(res).toBe(true); 299 | }); 300 | 301 | test("should not add into DB if autoSave is disabled", async () => { 302 | e.enableAutoSave(false); 303 | 304 | await e.addGroupingPolicy("role1", "user2"); 305 | 306 | const rules = await dbGetAll(); 307 | expect(rules).not.toContainEqual({ ptype: "g", rule: ["role1", "user2"] }); 308 | }); 309 | 310 | test("should add into DB if autoSave is enabled", async () => { 311 | e.enableAutoSave(true); 312 | 313 | await e.addGroupingPolicy("role1", "user3"); 314 | 315 | const rules = await dbGetAll(); 316 | expect(rules).toContainEqual({ ptype: "g", rule: ["role1", "user3"] }); 317 | }); 318 | 319 | }); 320 | 321 | describe(".removeGroupingPolicy()", () => { 322 | beforeAll(async () => { 323 | [m, a, e] = await buildEnforcer(); 324 | await importSampleData(); 325 | await e.loadPolicy(); 326 | }); 327 | afterAll(async () => { 328 | await cleanAdapter(a); 329 | await cleanDB(); 330 | }); 331 | 332 | test("should resolves with true", async () => { 333 | const res = await e.removeGroupingPolicy(...ROLES[0].rule); 334 | expect(res).toBe(true); 335 | }); 336 | 337 | test("should not remove from DB if autoSave is disabled", async () => { 338 | e.enableAutoSave(false); 339 | 340 | await e.removeGroupingPolicy(...ROLES[1].rule); 341 | 342 | const rules = await dbGetAll(); 343 | expect(rules).toContainEqual(ROLES[1]); 344 | }); 345 | 346 | test("should remove from DB if autoSave is enabled", async () => { 347 | e.enableAutoSave(true); 348 | 349 | await e.removeGroupingPolicy(...ROLES[2].rule); 350 | 351 | const rules = await dbGetAll(); 352 | expect(rules).not.toContainEqual(ROLES[2]); 353 | }); 354 | 355 | }); 356 | 357 | describe(".removeFilteredGroupingPolicy()", () => { 358 | beforeAll(async () => { 359 | [m, a, e] = await buildEnforcer(); 360 | await importSampleData(); 361 | await e.loadPolicy(); 362 | }); 363 | afterAll(async () => { 364 | await cleanAdapter(a); 365 | await cleanDB(); 366 | }); 367 | 368 | test("should resolves with true", async () => { 369 | const res = await e.removeFilteredGroupingPolicy(1, ROLES[0].rule[1]); 370 | expect(res).toBe(true); 371 | }); 372 | 373 | test("should not remove from DB if autoSave is disabled", async () => { 374 | e.enableAutoSave(false); 375 | 376 | await e.removeFilteredGroupingPolicy(1, ROLES[2].rule[1]); 377 | 378 | const rules = await dbGetAll(); 379 | expect(rules).toEqual( 380 | expect.arrayContaining(ROLES.filter(p => p.rule[1] === ROLES[2].rule[1])) 381 | ); 382 | }); 383 | 384 | test("should remove from DB if autoSave is enabled", async () => { 385 | e.enableAutoSave(true); 386 | 387 | await e.removeFilteredGroupingPolicy(1, ROLES[4].rule[1]); 388 | 389 | const rules = await dbGetAll(); 390 | expect(rules).toEqual( 391 | expect.not.arrayContaining(ROLES.filter(p => p.rule[1] === ROLES[4].rule[1])) 392 | ); 393 | }); 394 | 395 | }); 396 | 397 | describe(".savePolicy()", () => { 398 | beforeAll(async () => { 399 | [m, a, e] = await buildEnforcer(); 400 | a.enabledFiltered(false); 401 | }); 402 | afterAll(() => Promise.all([ 403 | cleanDB(), 404 | cleanAdapter(a), 405 | m.clearPolicy() 406 | ])); 407 | 408 | test("should resolves with true", async () => { 409 | m.addPolicy("p", "p", ["alice", "data", "read"]); 410 | const res = await e.savePolicy(); 411 | expect(res).toBe(true); 412 | }); 413 | 414 | test("should add policies in DB", async () => { 415 | m.addPolicy("p", "p", ["alice", "data", "read"]); 416 | m.addPolicy("p", "p", ["bob", "data", "write"]); 417 | m.addPolicy("p", "p", ["john", "data", "delete"]); 418 | m.addPolicy("g", "g", ["john", "role:admin"]); 419 | 420 | await a.savePolicy(m); 421 | 422 | const rules = await dbGetAll(); 423 | expect(rules).toEqual( 424 | expect.arrayContaining([ 425 | { ptype: "p", rule: ["alice", "data", "read"] }, 426 | { ptype: "p", rule: ["bob", "data", "write"] }, 427 | { ptype: "p", rule: ["john", "data", "delete"] }, 428 | { ptype: "g", rule: ["john", "role:admin"] } 429 | ]) 430 | ); 431 | }); 432 | 433 | test("should clear database before syncing", async () => { 434 | await importSampleData(); 435 | 436 | m.addPolicy("p", "p", ["alice", "data", "read"]); 437 | m.addPolicy("p", "p", ["bob", "data", "write"]); 438 | m.addPolicy("p", "p", ["john", "data", "delete"]); 439 | 440 | await a.savePolicy(m); 441 | 442 | const rules = await dbGetAll(); 443 | expect(rules).toEqual( 444 | expect.not.arrayContaining(POLICIES) 445 | ); 446 | }); 447 | 448 | test("should rejects if adapter.isFiltered is enabled", async () => { 449 | a.enabledFiltered(true); 450 | 451 | await expect(e.savePolicy()) 452 | .rejects.toBeInstanceOf(Error); 453 | }); 454 | 455 | }); 456 | 457 | describe(".enforce()", () => { 458 | beforeAll(async () => { 459 | [m, a, e] = await buildEnforcer(); 460 | await importSampleData(); 461 | await e.loadPolicy(); 462 | }); 463 | afterAll(async () => await Promise.all([ 464 | cleanAdapter(a), 465 | cleanDB() 466 | ])); 467 | 468 | test("should allow user1 permission to read data1", async () => { 469 | const res = await e.enforce("user1", "data1", "read"); 470 | expect(res).toBe(true); 471 | }); 472 | 473 | test("should allow user1 permission to write data1", async () => { 474 | const res = await e.enforce("user1", "data1", "write"); 475 | expect(res).toBe(true); 476 | }); 477 | 478 | test("should deny user1 permission to delete data1", async () => { 479 | const res = await e.enforce("user1", "data1", "delete"); 480 | expect(res).toBe(false); 481 | }); 482 | 483 | test("should allow user1 permission to read any data", async () => { 484 | const res = await e.enforce("user1", "dataany", "read"); 485 | expect(res).toBe(true); 486 | }); 487 | 488 | test("should allow user11 permission to do anything on any data", async () => { 489 | const res = await e.enforce("user11", "dataany", "actionany"); 490 | expect(res).toBe(true); 491 | }); 492 | 493 | }); 494 | 495 | }); --------------------------------------------------------------------------------