├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .pretiierignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── classes │ ├── accesscontrol.html │ ├── aclmodule.html │ └── aclservice.html ├── index.html ├── interfaces │ ├── aclcheckoptions.html │ ├── aclcontext.html │ ├── aclmoduleoptions.html │ ├── aclroles.html │ ├── aclrule.html │ └── aclrulescreatoroptions.html └── modules.html ├── package.json ├── src ├── constants.ts ├── decorators.ts ├── index.ts ├── interfaces.ts ├── module.ts ├── service.ts └── test │ └── service.spec.ts ├── tsconfig.json ├── typedoc.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "ignorePatterns": ["dist/test/*", "typedoc.js"], 9 | "extends": ["standard-with-typescript", "prettier"], 10 | "root": true, 11 | "rules": { 12 | "@typescript-eslint/explicit-function-return-type": "off", 13 | "@typescript-eslint/no-explicit-any": "off", 14 | "@typescript-eslint/no-empty-interface": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=M754R7Y5FE6DN&source=url'] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Node version 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Git Release 8 | 9 | jobs: 10 | build: 11 | name: Create Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@master 16 | - name: Create Release 17 | id: create_release 18 | uses: actions/create-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: Release ${{ github.ref }} 24 | draft: false 25 | prerelease: false 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [12.x, 13.x, 14.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and test 20 | run: | 21 | yarn install 22 | yarn run lint 23 | yarn run build 24 | yarn run test:cov 25 | - name: Codecov 26 | uses: codecov/codecov-action@v1.0.5 27 | with: 28 | token: ${{ secrets.CODECOV_TOKEN }} 29 | file: ./coverage/coverage-final.json 30 | fail_ci_if_error: true 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage 4 | .DS_Store 5 | .yarn* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | docs 3 | tsconfig.* 4 | .eslintrc 5 | typedoc.js 6 | coverage 7 | .prettierrc 8 | .pretiierignore 9 | yarn.lock 10 | .vscode 11 | *.tgz 12 | .github 13 | renovate.json 14 | dist/test 15 | .yarn* 16 | -------------------------------------------------------------------------------- /.pretiierignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | **/*.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 4, 4 | "semi": true, 5 | "trailingComma": "none", 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach", 8 | "port": 9229 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "eslint.validate": ["typescript", "javascript"], 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pop-Code 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nestjs ACL 2 | 3 | [![Actions Status](https://github.com/Pop-Code/nestjs-acl/workflows/CI/badge.svg)](https://github.com/Pop-Code/nestjs-acl/actions) 4 | [![codecov](https://codecov.io/gh/Pop-Code/nestjs-acl/branch/master/graph/badge.svg)][codecov] 5 | [![NPM Downloads](https://img.shields.io/npm/dm/nestjs-acl.svg?style=flat)][npmchart] 6 | ![node](https://img.shields.io/node/v/nestjs-acl) 7 | ![npm (tag)](https://img.shields.io/npm/v/nestjs-acl/latest) 8 | ![npm peer dependency version (scoped)](https://img.shields.io/npm/dependency-version/nestjs-console/peer/@nestjs/core) 9 | 10 | ## Why the nestjs-acl module? 11 | 12 | The [nestjs-acl][npm] module purpose is to check several access rules at the same time, depending on the current context. 13 | It allows to store, manage and check scenarios of access rules and to check them at any time. 14 | 15 | In our case, we needed to finely filter access to different types of resources for different types of users. 16 | Because some scenarios are repetitive, it was important to store these rules without repeating tones of code lines. 17 | 18 | [nestjs-acl][npm] does not replace the nest-access-control module, they are fully compatible and complementary. 19 | nest-access-control is a module to protect methods using decorators. It also allows you to implement your own access logic using the Nestjs Guards, which covers most cases. 20 | 21 | ## Install 22 | 23 | #### Using npm 24 | 25 | `npm install nestjs-acl --save` 26 | 27 | #### Using yarn 28 | 29 | `yarn add nestjs-acl` 30 | 31 | ### Prepare the roles 32 | 33 | Create a file that exports an AccessControl instance. 34 | You can refer to the npm package [accesscontrol] to learn how to manipulate roles 35 | 36 | _Note: The roles instance could be manipulate at runtime_ 37 | 38 | ```ts 39 | // roles.ts 40 | import { AccessControl } from 'nestjs-acl'; 41 | 42 | // See npm package accesscontrol for details 43 | export const roles = new AccessControl({ 44 | ADMIN: { 45 | doSomething: { 46 | 'create:any': ['*'] 47 | }, 48 | doSomethingElse: { 49 | 'create:any': ['*'] 50 | } 51 | }, 52 | USER: { 53 | doSomething: { 54 | 'create:own': ['*'] 55 | } 56 | } 57 | }); 58 | ``` 59 | 60 | ### Create and register AclRulesCreators 61 | 62 | 1. The AclService will search for matching AclRulesCreator and execute them passing the context. 63 | 2. If a AclRulesCreator is not found, the check passes (only if option rejectIfNoRule === false) 64 | 3. If a AclRulesCreator is found, The creator is executed. if option rejectIfNoRule === true and no rule was returned by the creator, the check will fail. 65 | 4. Returned Rules from AclRulesCreator are tested 66 | - The first rule that is granted will validate the test. 67 | - The first rule that throw an Error will stop the chain and invalidate the test. 68 | - Rules returning false are ignored 69 | - Rules returning Error are concatenated, returned as a single Error if no granted rule was found 70 | 71 | ```ts 72 | // rules.ts 73 | 74 | /** 75 | * Based on roles 76 | * users with the USER role will pass this scenarios only if they own the data (check returns true) 77 | * users with the ADMIN role will pass this scenarios 78 | */ 79 | export const userCanDoSomething = (opts) => { 80 | const { 81 | context: { user }, // the context passed to the test 82 | data, // the data passed to the test 83 | sourceData // the source data passed to the test 84 | } = opts; 85 | return [ 86 | // rule 1 87 | { 88 | req: opts.rolesBuilder.can(opts.context.user.roles).createOwn('doSomething'), 89 | // do an extra check if with context 90 | check: () => opts.data.user === context.user 91 | }, 92 | // rule 2 93 | { 94 | req: opts.rolesBuilder.can(opts.context.user.roles).createAny('doSomething') 95 | } 96 | ]; 97 | }; 98 | 99 | /** 100 | * Based on roles, only users with ADMIN role will be ale to pass this scenarios 101 | */ 102 | export const userCanDoSomethingElse = (opts) => { 103 | return [ 104 | { 105 | req: opts.rolesBuilder.can(opts.context.user.roles).createAny('doSomethingElse') 106 | } 107 | ]; 108 | }; 109 | ``` 110 | 111 | ### Import and register the AclModule module 112 | 113 | The AclModule is global and need to be registered once, imported modules can inject the AclService without the need to register the module again. 114 | 115 | ```ts 116 | // mymodule.ts 117 | import { AclModule, AclService } from 'nestjs-acl'; 118 | import { roles } from './roles'; 119 | import { userCanDoSomething, userCanDoSomethingElse } from './rules'; 120 | import { MyProvider } from './provider'; 121 | 122 | @Module({ 123 | imports: [AclModule.register(roles), MyProvider] 124 | }) 125 | export class MyModule { 126 | construtor(protected acl: AclService) { 127 | // register acl rules creators 128 | this.acl 129 | .registerRules('userCanDoSomething', userCanDoSomething) 130 | .registerRules('userCanDoSomethingElse', userCanDoSomethingElse); 131 | } 132 | } 133 | ``` 134 | 135 | ### Using the AclService 136 | 137 | In your modules, providers or controllers you can now inject the AclService instance and use it to check acl rules. 138 | 139 | ```ts 140 | // myprovider.ts 141 | import { Injectable } from '@nestjs/common'; 142 | import { AclService } from 'nestjs-acl'; 143 | 144 | @Injectable() 145 | export class MyProvider { 146 | constructor(protected acl: AclService) {} 147 | 148 | /** 149 | * This method is protected by a rule with id userCanDoSomething 150 | */ 151 | async doSomething(user: AclRoles) { 152 | const data = { foo: 'bar', user }; 153 | 154 | // The AclService will throw a ForbiddenException if the check fails. 155 | const { rule, data } = await this.acl.check({ 156 | id: 'userCanDoSomething', 157 | data, 158 | context: { 159 | user: { 160 | roles: user.roles 161 | } 162 | } 163 | }); 164 | 165 | // Do something... 166 | } 167 | 168 | /** 169 | * This method is protected by a rule with id userCanDoSomethingElse 170 | */ 171 | async doSomethingElse(user: AclRoles) { 172 | const { rule, data } = await this.acl.check({ 173 | id: 'userCanDoSomethingElse', 174 | context: { 175 | user: { 176 | roles: user.roles 177 | } 178 | } 179 | }); 180 | 181 | // Do something else... 182 | } 183 | } 184 | ``` 185 | 186 | ### [API DOCUMENTATION][doclink] 187 | 188 | [npm]: https://www.npmjs.com/package/nestjs-acl 189 | [npmchart]: https://npmcharts.com/compare/nestjs-acl?minimal=true 190 | [ci]: https://circleci.com/gh/Pop-Code/nestjs-acl 191 | [codecov]: https://codecov.io/gh/Pop-Code/nestjs-acl 192 | [doclink]: https://pop-code.github.io/nestjs-acl 193 | [accesscontrol]: https://www.npmjs.com/package/commander 194 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pop-Code/nestjs-acl/71446e94ba727d376aba811d06a24c5b4d3de332/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pop-Code/nestjs-acl/71446e94ba727d376aba811d06a24c5b4d3de332/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pop-Code/nestjs-acl/71446e94ba727d376aba811d06a24c5b4d3de332/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pop-Code/nestjs-acl/71446e94ba727d376aba811d06a24c5b4d3de332/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pop-Code/nestjs-acl/71446e94ba727d376aba811d06a24c5b4d3de332/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /docs/assets/js/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = {"kinds":{"32":"Variable","64":"Function","128":"Class","256":"Interface","512":"Constructor","1024":"Property","2048":"Method","65536":"Type literal","4194304":"Type alias"},"rows":[{"id":0,"kind":32,"name":"ROLES_BUILDER_TOKEN","url":"modules.html#roles_builder_token","classes":"tsd-kind-variable"},{"id":1,"kind":32,"name":"OPTIONS_TOKEN","url":"modules.html#options_token","classes":"tsd-kind-variable"},{"id":2,"kind":64,"name":"InjectRolesBuilder","url":"modules.html#injectrolesbuilder","classes":"tsd-kind-function"},{"id":3,"kind":64,"name":"InjectOptions","url":"modules.html#injectoptions","classes":"tsd-kind-function"},{"id":4,"kind":256,"name":"AclModuleOptions","url":"interfaces/aclmoduleoptions.html","classes":"tsd-kind-interface"},{"id":5,"kind":1024,"name":"rejectIfNoRule","url":"interfaces/aclmoduleoptions.html#rejectifnorule","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclModuleOptions"},{"id":6,"kind":256,"name":"AclRoles","url":"interfaces/aclroles.html","classes":"tsd-kind-interface tsd-has-type-parameter"},{"id":7,"kind":1024,"name":"roles","url":"interfaces/aclroles.html#roles","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclRoles"},{"id":8,"kind":4194304,"name":"CheckReturnType","url":"modules.html#checkreturntype","classes":"tsd-kind-type-alias"},{"id":9,"kind":256,"name":"AclRule","url":"interfaces/aclrule.html","classes":"tsd-kind-interface"},{"id":10,"kind":1024,"name":"req","url":"interfaces/aclrule.html#req","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclRule"},{"id":11,"kind":1024,"name":"res","url":"interfaces/aclrule.html#res","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclRule"},{"id":12,"kind":1024,"name":"check","url":"interfaces/aclrule.html#check","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclRule"},{"id":13,"kind":65536,"name":"__type","url":"interfaces/aclrule.html#__type","classes":"tsd-kind-type-literal tsd-parent-kind-interface","parent":"AclRule"},{"id":14,"kind":256,"name":"AclContext","url":"interfaces/aclcontext.html","classes":"tsd-kind-interface tsd-has-type-parameter"},{"id":15,"kind":1024,"name":"user","url":"interfaces/aclcontext.html#user","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclContext"},{"id":16,"kind":256,"name":"AclCheckOptions","url":"interfaces/aclcheckoptions.html","classes":"tsd-kind-interface tsd-has-type-parameter"},{"id":17,"kind":1024,"name":"id","url":"interfaces/aclcheckoptions.html#id","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclCheckOptions"},{"id":18,"kind":1024,"name":"context","url":"interfaces/aclcheckoptions.html#context","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclCheckOptions"},{"id":19,"kind":1024,"name":"data","url":"interfaces/aclcheckoptions.html#data","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclCheckOptions"},{"id":20,"kind":1024,"name":"sourceData","url":"interfaces/aclcheckoptions.html#sourcedata","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclCheckOptions"},{"id":21,"kind":1024,"name":"message","url":"interfaces/aclcheckoptions.html#message","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclCheckOptions"},{"id":22,"kind":1024,"name":"rejectIfNoRule","url":"interfaces/aclcheckoptions.html#rejectifnorule","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclCheckOptions"},{"id":23,"kind":256,"name":"AclRulesCreatorOptions","url":"interfaces/aclrulescreatoroptions.html","classes":"tsd-kind-interface tsd-has-type-parameter"},{"id":24,"kind":1024,"name":"rolesBuilder","url":"interfaces/aclrulescreatoroptions.html#rolesbuilder","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"AclRulesCreatorOptions"},{"id":25,"kind":1024,"name":"id","url":"interfaces/aclrulescreatoroptions.html#id","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"AclRulesCreatorOptions"},{"id":26,"kind":1024,"name":"context","url":"interfaces/aclrulescreatoroptions.html#context","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"AclRulesCreatorOptions"},{"id":27,"kind":1024,"name":"data","url":"interfaces/aclrulescreatoroptions.html#data","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"AclRulesCreatorOptions"},{"id":28,"kind":1024,"name":"sourceData","url":"interfaces/aclrulescreatoroptions.html#sourcedata","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"AclRulesCreatorOptions"},{"id":29,"kind":1024,"name":"message","url":"interfaces/aclrulescreatoroptions.html#message","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"AclRulesCreatorOptions"},{"id":30,"kind":1024,"name":"rejectIfNoRule","url":"interfaces/aclrulescreatoroptions.html#rejectifnorule","classes":"tsd-kind-property tsd-parent-kind-interface tsd-is-inherited","parent":"AclRulesCreatorOptions"},{"id":31,"kind":4194304,"name":"AclRulesCreator","url":"modules.html#aclrulescreator","classes":"tsd-kind-type-alias tsd-has-type-parameter"},{"id":32,"kind":65536,"name":"__type","url":"modules.html#aclrulescreator.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"AclRulesCreator"},{"id":33,"kind":128,"name":"AclModule","url":"classes/aclmodule.html","classes":"tsd-kind-class"},{"id":34,"kind":2048,"name":"register","url":"classes/aclmodule.html#register","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"AclModule"},{"id":35,"kind":512,"name":"constructor","url":"classes/aclmodule.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"AclModule"},{"id":36,"kind":128,"name":"AclService","url":"classes/aclservice.html","classes":"tsd-kind-class"},{"id":37,"kind":512,"name":"constructor","url":"classes/aclservice.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"AclService"},{"id":38,"kind":1024,"name":"log","url":"classes/aclservice.html#log","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-protected","parent":"AclService"},{"id":39,"kind":1024,"name":"rules","url":"classes/aclservice.html#rules","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-protected","parent":"AclService"},{"id":40,"kind":1024,"name":"rolesBuilder","url":"classes/aclservice.html#rolesbuilder","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-protected","parent":"AclService"},{"id":41,"kind":1024,"name":"options","url":"classes/aclservice.html#options","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-protected","parent":"AclService"},{"id":42,"kind":2048,"name":"getRolesBuilder","url":"classes/aclservice.html#getrolesbuilder","classes":"tsd-kind-method tsd-parent-kind-class","parent":"AclService"},{"id":43,"kind":1024,"name":"globalOptions","url":"classes/aclservice.html#globaloptions","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-protected","parent":"AclService"},{"id":44,"kind":2048,"name":"setGlobalOptions","url":"classes/aclservice.html#setglobaloptions","classes":"tsd-kind-method tsd-parent-kind-class","parent":"AclService"},{"id":45,"kind":2048,"name":"registerRules","url":"classes/aclservice.html#registerrules","classes":"tsd-kind-method tsd-parent-kind-class","parent":"AclService"},{"id":46,"kind":2048,"name":"check","url":"classes/aclservice.html#check","classes":"tsd-kind-method tsd-parent-kind-class tsd-has-type-parameter","parent":"AclService"},{"id":47,"kind":2048,"name":"isValidRule","url":"classes/aclservice.html#isvalidrule","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-protected","parent":"AclService"}],"index":{"version":"2.3.9","fields":["name","parent"],"fieldVectors":[["name/0",[0,34.864]],["parent/0",[]],["name/1",[1,34.864]],["parent/1",[]],["name/2",[2,34.864]],["parent/2",[]],["name/3",[3,34.864]],["parent/3",[]],["name/4",[4,29.755]],["parent/4",[]],["name/5",[5,26.391]],["parent/5",[4,2.547]],["name/6",[6,29.755]],["parent/6",[]],["name/7",[7,34.864]],["parent/7",[6,2.547]],["name/8",[8,34.864]],["parent/8",[]],["name/9",[9,21.871]],["parent/9",[]],["name/10",[10,34.864]],["parent/10",[9,1.872]],["name/11",[11,34.864]],["parent/11",[9,1.872]],["name/12",[12,29.755]],["parent/12",[9,1.872]],["name/13",[13,29.755]],["parent/13",[9,1.872]],["name/14",[14,29.755]],["parent/14",[]],["name/15",[15,34.864]],["parent/15",[14,2.547]],["name/16",[16,18.769]],["parent/16",[]],["name/17",[17,29.755]],["parent/17",[16,1.606]],["name/18",[18,29.755]],["parent/18",[16,1.606]],["name/19",[19,29.755]],["parent/19",[16,1.606]],["name/20",[20,29.755]],["parent/20",[16,1.606]],["name/21",[21,29.755]],["parent/21",[16,1.606]],["name/22",[5,26.391]],["parent/22",[16,1.606]],["name/23",[22,17.518]],["parent/23",[]],["name/24",[23,29.755]],["parent/24",[22,1.499]],["name/25",[17,29.755]],["parent/25",[22,1.499]],["name/26",[18,29.755]],["parent/26",[22,1.499]],["name/27",[19,29.755]],["parent/27",[22,1.499]],["name/28",[20,29.755]],["parent/28",[22,1.499]],["name/29",[21,29.755]],["parent/29",[22,1.499]],["name/30",[5,26.391]],["parent/30",[22,1.499]],["name/31",[24,29.755]],["parent/31",[]],["name/32",[13,29.755]],["parent/32",[24,2.547]],["name/33",[25,26.391]],["parent/33",[]],["name/34",[26,34.864]],["parent/34",[25,2.259]],["name/35",[27,29.755]],["parent/35",[25,2.259]],["name/36",[28,13.661]],["parent/36",[]],["name/37",[27,29.755]],["parent/37",[28,1.169]],["name/38",[29,34.864]],["parent/38",[28,1.169]],["name/39",[30,34.864]],["parent/39",[28,1.169]],["name/40",[23,29.755]],["parent/40",[28,1.169]],["name/41",[31,34.864]],["parent/41",[28,1.169]],["name/42",[32,34.864]],["parent/42",[28,1.169]],["name/43",[33,34.864]],["parent/43",[28,1.169]],["name/44",[34,34.864]],["parent/44",[28,1.169]],["name/45",[35,34.864]],["parent/45",[28,1.169]],["name/46",[12,29.755]],["parent/46",[28,1.169]],["name/47",[36,34.864]],["parent/47",[28,1.169]]],"invertedIndex":[["__type",{"_index":13,"name":{"13":{},"32":{}},"parent":{}}],["aclcheckoptions",{"_index":16,"name":{"16":{}},"parent":{"17":{},"18":{},"19":{},"20":{},"21":{},"22":{}}}],["aclcontext",{"_index":14,"name":{"14":{}},"parent":{"15":{}}}],["aclmodule",{"_index":25,"name":{"33":{}},"parent":{"34":{},"35":{}}}],["aclmoduleoptions",{"_index":4,"name":{"4":{}},"parent":{"5":{}}}],["aclroles",{"_index":6,"name":{"6":{}},"parent":{"7":{}}}],["aclrule",{"_index":9,"name":{"9":{}},"parent":{"10":{},"11":{},"12":{},"13":{}}}],["aclrulescreator",{"_index":24,"name":{"31":{}},"parent":{"32":{}}}],["aclrulescreatoroptions",{"_index":22,"name":{"23":{}},"parent":{"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"30":{}}}],["aclservice",{"_index":28,"name":{"36":{}},"parent":{"37":{},"38":{},"39":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{}}}],["check",{"_index":12,"name":{"12":{},"46":{}},"parent":{}}],["checkreturntype",{"_index":8,"name":{"8":{}},"parent":{}}],["constructor",{"_index":27,"name":{"35":{},"37":{}},"parent":{}}],["context",{"_index":18,"name":{"18":{},"26":{}},"parent":{}}],["data",{"_index":19,"name":{"19":{},"27":{}},"parent":{}}],["getrolesbuilder",{"_index":32,"name":{"42":{}},"parent":{}}],["globaloptions",{"_index":33,"name":{"43":{}},"parent":{}}],["id",{"_index":17,"name":{"17":{},"25":{}},"parent":{}}],["injectoptions",{"_index":3,"name":{"3":{}},"parent":{}}],["injectrolesbuilder",{"_index":2,"name":{"2":{}},"parent":{}}],["isvalidrule",{"_index":36,"name":{"47":{}},"parent":{}}],["log",{"_index":29,"name":{"38":{}},"parent":{}}],["message",{"_index":21,"name":{"21":{},"29":{}},"parent":{}}],["options",{"_index":31,"name":{"41":{}},"parent":{}}],["options_token",{"_index":1,"name":{"1":{}},"parent":{}}],["register",{"_index":26,"name":{"34":{}},"parent":{}}],["registerrules",{"_index":35,"name":{"45":{}},"parent":{}}],["rejectifnorule",{"_index":5,"name":{"5":{},"22":{},"30":{}},"parent":{}}],["req",{"_index":10,"name":{"10":{}},"parent":{}}],["res",{"_index":11,"name":{"11":{}},"parent":{}}],["roles",{"_index":7,"name":{"7":{}},"parent":{}}],["roles_builder_token",{"_index":0,"name":{"0":{}},"parent":{}}],["rolesbuilder",{"_index":23,"name":{"24":{},"40":{}},"parent":{}}],["rules",{"_index":30,"name":{"39":{}},"parent":{}}],["setglobaloptions",{"_index":34,"name":{"44":{}},"parent":{}}],["sourcedata",{"_index":20,"name":{"20":{},"28":{}},"parent":{}}],["user",{"_index":15,"name":{"15":{}},"parent":{}}]],"pipeline":[]}} -------------------------------------------------------------------------------- /docs/classes/aclmodule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclModule | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Class AclModule

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

The AclModule will do 2 things

72 |
    73 |
  1. He will check acl depending on user roles
  2. 74 |
  3. He will check acl depending on custom rules
  4. 75 |
76 |
77 |
78 |
79 |
80 |

Hierarchy

81 |
    82 |
  • 83 | AclModule 84 |
  • 85 |
86 |
87 |
88 |

Index

89 |
90 |
91 |
92 |

Constructors

93 | 96 |
97 |
98 |

Methods

99 | 102 |
103 |
104 |
105 |
106 |
107 |

Constructors

108 |
109 | 110 |

constructor

111 | 114 |
    115 |
  • 116 | 118 |

    Returns AclModule

    119 |
  • 120 |
121 |
122 |
123 |
124 |

Methods

125 |
126 | 127 |

Static register

128 | 131 |
    132 |
  • 133 | 138 |
    139 |
    140 |

    Register the AclModule.

    141 |
    142 |
    143 |

    Parameters

    144 | 152 |

    Returns Promise<DynamicModule>

    153 |
  • 154 |
155 |
156 |
157 |
158 | 228 |
229 |
230 |
231 |
232 |

Legend

233 |
234 |
    235 |
  • Constructor
  • 236 |
  • Method
  • 237 |
238 |
    239 |
  • Protected property
  • 240 |
  • Protected method
  • 241 |
242 |
    243 |
  • Property
  • 244 |
245 |
    246 |
  • Static method
  • 247 |
248 |
249 |
250 |
251 |
252 | 253 | 254 | -------------------------------------------------------------------------------- /docs/classes/aclservice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclService | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Class AclService

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Hierarchy

70 |
    71 |
  • 72 | AclService 73 |
  • 74 |
75 |
76 |
77 |

Index

78 |
79 |
80 |
81 |

Constructors

82 | 85 |
86 |
87 |

Properties

88 | 95 |
96 |
97 |

Methods

98 | 105 |
106 |
107 |
108 |
109 |
110 |

Constructors

111 |
112 | 113 |

constructor

114 | 117 | 136 |
137 |
138 |
139 |

Properties

140 |
141 | 142 |

Protected globalOptions

143 |
globalOptions: AclModuleOptions & {} = {}
144 | 149 |
150 |
151 |

A key/value pair object of the global options to pass to rule creator during check. 152 | All rules would be able to read those object keys

153 |
154 |
155 |
156 |
157 | 158 |

Protected log

159 |
log: Debugger = ...
160 | 165 |
166 |
167 |

The log function

168 |
169 |
170 |
171 |
172 | 173 |

Protected options

174 | 175 | 177 |
178 |
179 | 180 |

Protected Readonly rolesBuilder

181 |
rolesBuilder: AccessControl
182 | 184 |
185 |
186 | 187 |

Protected rules

188 |
rules: Map<string, AclRulesCreator<any, any, any>> = ...
189 | 194 |
195 |
196 |

A Map of AclRules

197 |
198 |
199 |
200 |
201 |
202 |

Methods

203 |
204 | 205 |

check

206 | 209 |
    210 |
  • 211 | 216 |
    217 |
    218 |

    This method will check that each rules returned are granted.

    219 |
    220 |
    221 |
    throws
    222 |

    Error|ForbiddenException If access is not granted

    223 |
    224 |
    225 |
    226 |

    Type parameters

    227 |
      228 |
    • 229 |

      D = any

      230 |
    • 231 |
    • 232 |

      S = any

      233 |
    • 234 |
    235 |

    Parameters

    236 | 241 |

    Returns Promise<{ data?: D; rule?: AclRule }>

    242 |
  • 243 |
244 |
245 |
246 | 247 |

getRolesBuilder

248 | 251 |
    252 |
  • 253 | 258 |
    259 |
    260 |

    Get the role builder instance

    261 |
    262 |
    263 |

    Returns AccessControl

    264 |
  • 265 |
266 |
267 |
268 | 269 |

Protected isValidRule

270 |
    271 |
  • isValidRule(rule: AclRule): Promise<undefined | boolean | Error>
  • 272 |
273 |
    274 |
  • 275 | 280 |
    281 |
    282 |

    Check if an AclRule is valid

    283 |
    284 |
    285 |

    Parameters

    286 |
      287 |
    • 288 |
      rule: AclRule
      289 |
    • 290 |
    291 |

    Returns Promise<undefined | boolean | Error>

    292 |
  • 293 |
294 |
295 |
296 | 297 |

registerRules

298 | 301 |
    302 |
  • 303 | 308 |
    309 |
    310 |

    Register an AclRulesCreator

    311 |
    312 |
    313 |

    Parameters

    314 |
      315 |
    • 316 |
      id: string
      317 |
      318 |

      The id of this rules creator

      319 |
      320 |
    • 321 |
    • 322 |
      creator: AclRulesCreator<any, any, any>
      323 |
      324 |

      The creator function

      325 |
      326 |
    • 327 |
    328 |

    Returns AclService

    329 |
  • 330 |
331 |
332 |
333 | 334 |

setGlobalOptions

335 | 338 |
    339 |
  • 340 | 345 |
    346 |
    347 |

    Set the global options

    348 |
    349 |
    350 |
    see
    351 |

    globalOptions

    352 |
    353 |
    354 |
    355 |

    Parameters

    356 | 361 |

    Returns void

    362 |
  • 363 |
364 |
365 |
366 |
367 | 464 |
465 |
466 |
467 |
468 |

Legend

469 |
470 |
    471 |
  • Constructor
  • 472 |
  • Method
  • 473 |
474 |
    475 |
  • Protected property
  • 476 |
  • Protected method
  • 477 |
478 |
    479 |
  • Property
  • 480 |
481 |
    482 |
  • Static method
  • 483 |
484 |
485 |
486 |
487 |
488 | 489 | 490 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 |

Nestjs ACL Documentation

54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 |

Nestjs ACL

63 |
64 |

Actions Status 65 | codecov 66 | NPM Downloads 67 | node 68 | npm (tag) 69 | npm peer dependency version (scoped)

70 | 71 |

Why the nestjs-acl module?

72 |
73 |

The nestjs-acl module purpose is to check several access rules at the same time, depending on the current context.
It allows to store, manage and check scenarios of access rules and to check them at any time.

74 |

In our case, we needed to finely filter access to different types of resources for different types of users. 75 | Because some scenarios are repetitive, it was important to store these rules without repeating tones of code lines.

76 |

nestjs-acl does not replace the nest-access-control module, they are fully compatible and complementary. 77 | nest-access-control is a module to protect methods using decorators. It also allows you to implement your own access logic using the Nestjs Guards, which covers most cases.

78 | 79 |

Install

80 |
81 | 82 |

Using npm

83 |
84 |

npm install nestjs-acl --save

85 | 86 |

Using yarn

87 |
88 |

yarn add nestjs-acl

89 | 90 |

Prepare the roles

91 |
92 |

Create a file that exports an AccessControl instance. 93 | You can refer to the npm package accesscontrol to learn how to manipulate roles

94 |

Note: The roles instance could be manipulate at runtime

95 |
// roles.ts
 96 | import { AccessControl } from 'nestjs-acl';
 97 | 
 98 | // See npm package accesscontrol for details
 99 | export const roles = new AccessControl({
100 |     ADMIN: {
101 |         doSomething: {
102 |             'create:any': ['*']
103 |         },
104 |         doSomethingElse: {
105 |             'create:any': ['*']
106 |         }
107 |     },
108 |     USER: {
109 |         doSomething: {
110 |             'create:own': ['*']
111 |         }
112 |     }
113 | });
114 | 
115 | 116 |

Create and register AclRulesCreators

117 |
118 |
    119 |
  1. The AclService will search for matching AclRulesCreator and execute them passing the context.
  2. 120 |
  3. If a AclRulesCreator is not found, the check passes (only if option rejectIfNoRule === false)
  4. 121 |
  5. If a AclRulesCreator is found, The creator is executed. if option rejectIfNoRule === true and no rule was returned by the creator, the check will fail.
  6. 122 |
  7. Returned Rules from AclRulesCreator are tested
      123 |
    • The first rule that is granted will validate the test.
    • 124 |
    • The first rule that throw an Error will stop the chain and invalidate the test.
    • 125 |
    • Rules returning false are ignored
    • 126 |
    • Rules returning Error are concatenated, returned as a single Error if no granted rule was found
    • 127 |
    128 |
  8. 129 |
130 |
// rules.ts
131 | 
132 | /**
133 |  * Based on roles
134 |  * users with the USER role will pass this scenarios only if they own the data (check returns true)
135 |  * users with the ADMIN role will pass this scenarios
136 |  */
137 | export const userCanDoSomething = (opts) => {
138 |     const {
139 |         context: { user }, // the context passed to the test
140 |         data, // the data passed to the test
141 |         sourceData // the source data passed to the test
142 |     } = opts;
143 |     return [
144 |         // rule 1
145 |         {
146 |             req: opts.rolesBuilder.can(opts.context.user.roles).createOwn('doSomething'),
147 |             // do an extra check if with context
148 |             check: () => opts.data.user === context.user
149 |         },
150 |         // rule 2
151 |         {
152 |             req: opts.rolesBuilder.can(opts.context.user.roles).createAny('doSomething')
153 |         }
154 |     ];
155 | };
156 | 
157 | /**
158 |  * Based on roles, only users with ADMIN role will be ale to pass this scenarios
159 |  */
160 | export const userCanDoSomethingElse = (opts) => {
161 |     return [
162 |         {
163 |             req: opts.rolesBuilder.can(opts.context.user.roles).createAny('doSomethingElse')
164 |         }
165 |     ];
166 | };
167 | 
168 | 169 |

Import and register the AclModule module

170 |
171 |

The AclModule is global and need to be registered once, imported modules can inject the AclService without the need to register the module again.

172 |
// mymodule.ts
173 | import { AclModule, AclService } from 'nestjs-acl';
174 | import { roles } from './roles';
175 | import { userCanDoSomething, userCanDoSomethingElse } from './rules';
176 | import { MyProvider } from './provider';
177 | 
178 | @Module({
179 |     imports: [AclModule.register(roles), MyProvider]
180 | })
181 | export class MyModule {
182 |     construtor(protected acl: AclService) {
183 |         // register acl rules creators
184 |         this.acl
185 |             .registerRules('userCanDoSomething', userCanDoSomething)
186 |             .registerRules('userCanDoSomethingElse', userCanDoSomethingElse);
187 |     }
188 | }
189 | 
190 | 191 |

Using the AclService

192 |
193 |

In your modules, providers or controllers you can now inject the AclService instance and use it to check acl rules.

194 |
// myprovider.ts
195 | import { Injectable } from '@nestjs/common';
196 | import { AclService } from 'nestjs-acl';
197 | 
198 | @Injectable()
199 | export class MyProvider {
200 |     constructor(protected acl: AclService) {}
201 | 
202 |     /**
203 |      * This method is protected by a rule with id userCanDoSomething
204 |      */
205 |     async doSomething(user: AclRoles<string>) {
206 |         const data = { foo: 'bar', user };
207 | 
208 |         // The AclService will throw a ForbiddenException if the check fails.
209 |         const { rule, data } = await this.acl.check({
210 |             id: 'userCanDoSomething',
211 |             data,
212 |             context: {
213 |                 user: {
214 |                     roles: user.roles
215 |                 }
216 |             }
217 |         });
218 | 
219 |         // Do something...
220 |     }
221 | 
222 |     /**
223 |      * This method is protected by a rule with id userCanDoSomethingElse
224 |      */
225 |     async doSomethingElse(user: AclRoles<string>) {
226 |         const { rule, data } = await this.acl.check({
227 |             id: 'userCanDoSomethingElse',
228 |             context: {
229 |                 user: {
230 |                     roles: user.roles
231 |                 }
232 |             }
233 |         });
234 | 
235 |         // Do something else...
236 |     }
237 | }
238 | 
239 | 240 |

API DOCUMENTATION

241 | 242 | 243 |

[CHANGELOG][changelog]

244 |
245 |
246 |
247 | 305 |
306 |
307 |
308 |
309 |

Legend

310 |
311 |
    312 |
  • Constructor
  • 313 |
  • Method
  • 314 |
315 |
    316 |
  • Protected property
  • 317 |
  • Protected method
  • 318 |
319 |
    320 |
  • Property
  • 321 |
322 |
    323 |
  • Static method
  • 324 |
325 |
326 |
327 |
328 |
329 | 330 | 331 | -------------------------------------------------------------------------------- /docs/interfaces/aclcheckoptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclCheckOptions | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface AclCheckOptions<Data, Source, Context>

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

The options to pass to the checker

72 |
73 |
74 |
75 |
76 |

Type parameters

77 |
    78 |
  • 79 |

    Data = any

    80 |
  • 81 |
  • 82 |

    Source = any

    83 |
  • 84 |
  • 85 |

    Context: AclContext = any

    86 |
  • 87 |
88 |
89 |
90 |

Hierarchy

91 | 101 |
102 |
103 |

Indexable

104 |
[key: string]: any
105 |
106 |
107 |

Any other key/value you want to pass to the checker, those options will be readable in the rule creator function

108 |
109 |
110 |
111 |
112 |

Index

113 |
114 |
115 |
116 |

Properties

117 | 125 |
126 |
127 |
128 |
129 |
130 |

Properties

131 |
132 | 133 |

context

134 |
context: Context
135 | 140 |
141 |
142 |

The context

143 |
144 |
145 |
146 |
147 | 148 |

Optional data

149 |
data: Data
150 | 155 |
156 |
157 |

The data to check

158 |
159 |
160 |
161 |
162 | 163 |

id

164 |
id: string
165 | 170 |
171 |
172 |

The id of the rule

173 |
174 |
175 |
176 |
177 | 178 |

Optional message

179 |
message: string
180 | 185 |
186 |
187 |

A custom message to pass to the ForbiddenException

188 |
189 |
190 |
191 |
192 | 193 |

Optional rejectIfNoRule

194 |
rejectIfNoRule: boolean
195 | 200 |
201 |
202 |

Will reject the check if no rule creator found or if the creator returns no rule

203 |
204 |
205 |
206 |
207 | 208 |

Optional sourceData

209 |
sourceData: Source
210 | 215 |
216 |
217 |

The source data, used to represent the data that are modified

218 |
219 |
220 |
221 |
222 |
223 | 305 |
306 |
307 |
308 |
309 |

Legend

310 |
311 |
    312 |
  • Constructor
  • 313 |
  • Method
  • 314 |
315 |
    316 |
  • Protected property
  • 317 |
  • Protected method
  • 318 |
319 |
    320 |
  • Property
  • 321 |
322 |
    323 |
  • Static method
  • 324 |
325 |
326 |
327 |
328 |
329 | 330 | 331 | -------------------------------------------------------------------------------- /docs/interfaces/aclcontext.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclContext | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface AclContext<R, User>

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

The context of a request

72 |
73 |
74 |
75 |
76 |

Type parameters

77 |
    78 |
  • 79 |

    R = any

    80 |
  • 81 |
  • 82 |

    User: AclRoles<R> = any

    83 |
  • 84 |
85 |
86 |
87 |

Hierarchy

88 |
    89 |
  • 90 | AclContext 91 |
  • 92 |
93 |
94 |
95 |

Index

96 |
97 |
98 |
99 |

Properties

100 | 103 |
104 |
105 |
106 |
107 |
108 |

Properties

109 |
110 | 111 |

Optional user

112 |
user: User
113 | 118 |
119 |
120 |
121 | 188 |
189 |
190 |
191 |
192 |

Legend

193 |
194 |
    195 |
  • Constructor
  • 196 |
  • Method
  • 197 |
198 |
    199 |
  • Protected property
  • 200 |
  • Protected method
  • 201 |
202 |
    203 |
  • Property
  • 204 |
205 |
    206 |
  • Static method
  • 207 |
208 |
209 |
210 |
211 |
212 | 213 | 214 | -------------------------------------------------------------------------------- /docs/interfaces/aclmoduleoptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclModuleOptions | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface AclModuleOptions

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

The options of the module

72 |
73 |
74 |
75 |
76 |

Hierarchy

77 |
    78 |
  • 79 | AclModuleOptions 80 |
  • 81 |
82 |
83 |
84 |

Index

85 |
86 |
87 |
88 |

Properties

89 | 92 |
93 |
94 |
95 |
96 |
97 |

Properties

98 |
99 | 100 |

Optional rejectIfNoRule

101 |
rejectIfNoRule: boolean
102 | 107 |
108 |
109 |

Will reject the check if no rule creator found or if the creator returns no rule

110 |
111 |
112 |
113 |
114 |
115 | 182 |
183 |
184 |
185 |
186 |

Legend

187 |
188 |
    189 |
  • Constructor
  • 190 |
  • Method
  • 191 |
192 |
    193 |
  • Protected property
  • 194 |
  • Protected method
  • 195 |
196 |
    197 |
  • Property
  • 198 |
199 |
    200 |
  • Static method
  • 201 |
202 |
203 |
204 |
205 |
206 | 207 | 208 | -------------------------------------------------------------------------------- /docs/interfaces/aclroles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclRoles | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface AclRoles<Role>

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

The interface of the entity implementing roles. Usually this is your user entity

72 |
73 |
74 |
75 |
76 |

Type parameters

77 |
    78 |
  • 79 |

    Role

    80 |
  • 81 |
82 |
83 |
84 |

Hierarchy

85 |
    86 |
  • 87 | AclRoles 88 |
  • 89 |
90 |
91 |
92 |

Index

93 |
94 |
95 |
96 |

Properties

97 | 100 |
101 |
102 |
103 |
104 |
105 |

Properties

106 |
107 | 108 |

Optional roles

109 |
roles: Role[]
110 | 115 |
116 |
117 |
118 | 185 |
186 |
187 |
188 |
189 |

Legend

190 |
191 |
    192 |
  • Constructor
  • 193 |
  • Method
  • 194 |
195 |
    196 |
  • Protected property
  • 197 |
  • Protected method
  • 198 |
199 |
    200 |
  • Property
  • 201 |
202 |
    203 |
  • Static method
  • 204 |
205 |
206 |
207 |
208 |
209 | 210 | 211 | -------------------------------------------------------------------------------- /docs/interfaces/aclrule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclRule | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface AclRule

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

An Acl Rule

72 |
73 |
74 |
75 |
76 |

Hierarchy

77 |
    78 |
  • 79 | AclRule 80 |
  • 81 |
82 |
83 |
84 |

Index

85 |
86 |
87 |
88 |

Properties

89 | 94 |
95 |
96 |
97 |
98 |
99 |

Properties

100 |
101 | 102 |

Optional check

103 |
check: () => CheckReturnType
104 | 109 |
110 |

Type declaration

111 | 123 |
124 |
125 |
126 | 127 |

Optional req

128 |
req: Permission
129 | 134 |
135 |
136 | 137 |

Optional res

138 |
res: Permission
139 | 144 |
145 |
146 |
147 | 220 |
221 |
222 |
223 |
224 |

Legend

225 |
226 |
    227 |
  • Constructor
  • 228 |
  • Method
  • 229 |
230 |
    231 |
  • Protected property
  • 232 |
  • Protected method
  • 233 |
234 |
    235 |
  • Property
  • 236 |
237 |
    238 |
  • Static method
  • 239 |
240 |
241 |
242 |
243 |
244 | 245 | 246 | -------------------------------------------------------------------------------- /docs/interfaces/aclrulescreatoroptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AclRulesCreatorOptions | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface AclRulesCreatorOptions<Data, Source, Context>

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Th options the checker will receive as argument

72 |
73 |
74 |
75 |
76 |

Type parameters

77 |
    78 |
  • 79 |

    Data = any

    80 |
  • 81 |
  • 82 |

    Source = any

    83 |
  • 84 |
  • 85 |

    Context: AclContext = any

    86 |
  • 87 |
88 |
89 |
90 |

Hierarchy

91 |
    92 |
  • 93 | AclCheckOptions<Data, Source, Context> 94 |
      95 |
    • 96 | AclRulesCreatorOptions 97 |
    • 98 |
    99 |
  • 100 |
101 |
102 |
103 |

Index

104 |
105 |
106 |
107 |

Properties

108 | 117 |
118 |
119 |
120 |
121 |
122 |

Properties

123 |
124 | 125 |

context

126 |
context: Context
127 | 133 |
134 |
135 |

The context

136 |
137 |
138 |
139 |
140 | 141 |

Optional data

142 |
data: Data
143 | 149 |
150 |
151 |

The data to check

152 |
153 |
154 |
155 |
156 | 157 |

id

158 |
id: string
159 | 165 |
166 |
167 |

The id of the rule

168 |
169 |
170 |
171 |
172 | 173 |

Optional message

174 |
message: string
175 | 181 |
182 |
183 |

A custom message to pass to the ForbiddenException

184 |
185 |
186 |
187 |
188 | 189 |

Optional rejectIfNoRule

190 |
rejectIfNoRule: boolean
191 | 197 |
198 |
199 |

Will reject the check if no rule creator found or if the creator returns no rule

200 |
201 |
202 |
203 |
204 | 205 |

rolesBuilder

206 |
rolesBuilder: AccessControl
207 | 212 |
213 |
214 | 215 |

Optional sourceData

216 |
sourceData: Source
217 | 223 |
224 |
225 |

The source data, used to represent the data that are modified

226 |
227 |
228 |
229 |
230 |
231 | 316 |
317 |
318 |
319 |
320 |

Legend

321 |
322 |
    323 |
  • Constructor
  • 324 |
  • Method
  • 325 |
326 |
    327 |
  • Protected property
  • 328 |
  • Protected method
  • 329 |
330 |
    331 |
  • Property
  • 332 |
333 |
    334 |
  • Static method
  • 335 |
336 |
337 |
338 |
339 |
340 | 341 | 342 | -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nestjs ACL Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 |

Nestjs ACL Documentation

54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |

Index

62 |
63 |
64 |
65 |

Classes

66 | 71 |
72 |
73 |

Interfaces

74 | 82 |
83 |
84 |

Type aliases

85 | 89 |
90 |
91 |

Variables

92 | 96 |
97 |
98 |

Functions

99 | 103 |
104 |
105 |
106 |
107 |
108 |

Type aliases

109 |
110 | 111 |

AclRulesCreator

112 |
AclRulesCreator<Data, Source, Context>: (options: AclRulesCreatorOptions<Data, Source, Context>) => AclRule[] | Promise<AclRule[]>
113 | 118 |
119 |
120 |

The signature of a acl rule creator

121 |
122 |
123 |

Type parameters

124 |
    125 |
  • 126 |

    Data = any

    127 |
  • 128 |
  • 129 |

    Source = any

    130 |
  • 131 |
  • 132 |

    Context: AclContext = any

    133 |
  • 134 |
135 |
136 |

Type declaration

137 | 155 |
156 |
157 |
158 | 159 |

CheckReturnType

160 |
CheckReturnType: (boolean | Error | undefined) | Promise<boolean | Error | undefined>
161 | 166 |
167 |
168 |
169 |

Variables

170 |
171 | 172 |

Const OPTIONS_TOKEN

173 |
OPTIONS_TOKEN: "nestjs-acl-options" = 'nestjs-acl-options'
174 | 179 |
180 |
181 | 182 |

Const ROLES_BUILDER_TOKEN

183 |
ROLES_BUILDER_TOKEN: "nestjs-acl-roles" = 'nestjs-acl-roles'
184 | 189 |
190 |
191 |
192 |

Functions

193 |
194 | 195 |

InjectOptions

196 |
    197 |
  • InjectOptions(): (target: object, key: string | symbol, index?: number) => void
  • 198 |
199 |
    200 |
  • 201 | 206 |
    207 |
    208 |

    A nestjs param decorator to inject the options into providers

    209 |
    210 |
    211 |

    Returns (target: object, key: string | symbol, index?: number) => void

    212 |
      213 |
    • 214 |
        215 |
      • (target: object, key: string | symbol, index?: number): void
      • 216 |
      217 |
        218 |
      • 219 |

        Parameters

        220 |
          221 |
        • 222 |
          target: object
          223 |
        • 224 |
        • 225 |
          key: string | symbol
          226 |
        • 227 |
        • 228 |
          Optional index: number
          229 |
        • 230 |
        231 |

        Returns void

        232 |
      • 233 |
      234 |
    • 235 |
    236 |
  • 237 |
238 |
239 |
240 | 241 |

InjectRolesBuilder

242 |
    243 |
  • InjectRolesBuilder(): (target: object, key: string | symbol, index?: number) => void
  • 244 |
245 |
    246 |
  • 247 | 252 |
    253 |
    254 |

    A nestjs param decorator to inject the role builder instance into providers

    255 |
    256 |
    257 |

    Returns (target: object, key: string | symbol, index?: number) => void

    258 |
      259 |
    • 260 |
        261 |
      • (target: object, key: string | symbol, index?: number): void
      • 262 |
      263 |
        264 |
      • 265 |

        Parameters

        266 |
          267 |
        • 268 |
          target: object
          269 |
        • 270 |
        • 271 |
          key: string | symbol
          272 |
        • 273 |
        • 274 |
          Optional index: number
          275 |
        • 276 |
        277 |

        Returns void

        278 |
      • 279 |
      280 |
    • 281 |
    282 |
  • 283 |
284 |
285 |
286 |
287 | 345 |
346 |
347 |
348 |
349 |

Legend

350 |
351 |
    352 |
  • Constructor
  • 353 |
  • Method
  • 354 |
355 |
    356 |
  • Protected property
  • 357 |
  • Protected method
  • 358 |
359 |
    360 |
  • Property
  • 361 |
362 |
    363 |
  • Static method
  • 364 |
365 |
366 |
367 |
368 |
369 | 370 | 371 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-acl", 3 | "version": "0.2.6", 4 | "description": "A nestjs package to check acl rules at runtime", 5 | "author": "Rmannn ", 6 | "repository": "https://github.com/POP-CODE/nestjs-acl.git", 7 | "license": "MIT", 8 | "main": "dist/index.js", 9 | "scripts": { 10 | "build": "rm -Rf dist && tsc -b", 11 | "format": "prettier \"**/*.ts\" --ignore-path ./.prettierignore --write && git status", 12 | "doc": "rm -Rf ./docs && typedoc && touch ./docs/.nojekyll", 13 | "lint": "eslint src/**/**.ts", 14 | "test": "jest", 15 | "test:cov": "jest --coverage", 16 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" 17 | }, 18 | "peerDependencies": { 19 | "@nestjs/common": "^6 || ^7 || ^8" 20 | }, 21 | "dependencies": { 22 | "accesscontrol": "2.2.1", 23 | "debug": "4.3.2", 24 | "lodash": "4.17.21", 25 | "nest-access-control": "2.0.2" 26 | }, 27 | "devDependencies": { 28 | "@nestjs/common": "8.0.6", 29 | "@nestjs/core": "8.0.6", 30 | "@nestjs/testing": "8.0.6", 31 | "@types/debug": "4.1.7", 32 | "@types/jest": "27.0.1", 33 | "@types/node": "16.7.1", 34 | "@typescript-eslint/eslint-plugin": "4.29.3", 35 | "@typescript-eslint/parser": "4.29.3", 36 | "eslint": "7.32.0", 37 | "eslint-config-prettier": "8.3.0", 38 | "eslint-config-standard-with-typescript": "20.0.0", 39 | "eslint-plugin-import": "2.24.1", 40 | "eslint-plugin-node": "11.1.0", 41 | "eslint-plugin-prefer-arrow": "1.2.3", 42 | "eslint-plugin-promise": "5.1.0", 43 | "jest": "27.0.6", 44 | "prettier": "2.3.2", 45 | "reflect-metadata": "0.1.13", 46 | "rxjs": "7.3.0", 47 | "ts-jest": "27.0.5", 48 | "ts-node": "10.2.1", 49 | "tsconfig-paths": "3.10.1", 50 | "typedoc": "0.21.6", 51 | "typescript": "4.3.5" 52 | }, 53 | "jest": { 54 | "moduleFileExtensions": [ 55 | "js", 56 | "json", 57 | "ts" 58 | ], 59 | "rootDir": "src", 60 | "testRegex": ".spec.ts$", 61 | "transform": { 62 | "^.+\\.(t|j)s$": "ts-jest" 63 | }, 64 | "collectCoverageFrom": [ 65 | "**/*.{js,jsx,ts}", 66 | "!index.ts", 67 | "!**/test/**" 68 | ], 69 | "coverageDirectory": "../coverage" 70 | }, 71 | "engines": { 72 | "node": ">=0.10" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ROLES_BUILDER_TOKEN = 'nestjs-acl-roles'; 2 | export const OPTIONS_TOKEN = 'nestjs-acl-options'; 3 | -------------------------------------------------------------------------------- /src/decorators.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { OPTIONS_TOKEN, ROLES_BUILDER_TOKEN } from './constants'; 4 | 5 | /** 6 | * A nestjs param decorator to inject the role builder instance into providers 7 | */ 8 | export function InjectRolesBuilder() { 9 | return Inject(ROLES_BUILDER_TOKEN); 10 | } 11 | 12 | /** 13 | * A nestjs param decorator to inject the options into providers 14 | */ 15 | export function InjectOptions() { 16 | return Inject(OPTIONS_TOKEN); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module nestjs-acl 3 | */ 4 | 5 | export { AccessControl } from 'accesscontrol'; 6 | export * from './constants'; 7 | export * from './decorators'; 8 | export * from './interfaces'; 9 | export * from './module'; 10 | export * from './service'; 11 | 12 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { AccessControl, Permission } from 'accesscontrol'; 2 | 3 | /** 4 | * The options of the module 5 | */ 6 | export interface AclModuleOptions { 7 | /** 8 | * Will reject the check if no rule creator found or if the creator returns no rule 9 | */ 10 | rejectIfNoRule?: boolean 11 | } 12 | 13 | /** 14 | * The interface of the entity implementing roles. Usually this is your user entity 15 | */ 16 | export interface AclRoles { 17 | roles?: Role[]; 18 | } 19 | 20 | export type CheckReturnType = (boolean | Error | undefined) | Promise<(boolean | Error | undefined)>; 21 | 22 | /** 23 | * An Acl Rule 24 | */ 25 | export interface AclRule { 26 | req?: Permission; 27 | res?: Permission; 28 | check?: () => CheckReturnType; 29 | } 30 | 31 | /** 32 | * The context of a request 33 | */ 34 | export interface AclContext = any> { 35 | user?: User; 36 | } 37 | 38 | /** 39 | * The options to pass to the checker 40 | */ 41 | export interface AclCheckOptions { 42 | /** 43 | * The id of the rule 44 | */ 45 | id: string; 46 | 47 | /** 48 | * The context 49 | */ 50 | context: Context; 51 | 52 | /** 53 | * The data to check 54 | */ 55 | data?: Data; 56 | 57 | /** 58 | * The source data, used to represent the data that are modified 59 | */ 60 | sourceData?: Source; 61 | 62 | /** 63 | * A custom message to pass to the ForbiddenException 64 | */ 65 | message?: string; 66 | 67 | /** 68 | * Will reject the check if no rule creator found or if the creator returns no rule 69 | */ 70 | rejectIfNoRule?: boolean; 71 | 72 | /** 73 | * Any other key/value you want to pass to the checker, those options will be readable in the rule creator function 74 | */ 75 | [key: string]: any; 76 | } 77 | 78 | /** 79 | * Th options the checker will receive as argument 80 | */ 81 | export interface AclRulesCreatorOptions 82 | extends AclCheckOptions { 83 | rolesBuilder: AccessControl; 84 | } 85 | 86 | /** 87 | * The signature of a acl rule creator 88 | */ 89 | export type AclRulesCreator = ( 90 | options: AclRulesCreatorOptions 91 | ) => AclRule[] | Promise; 92 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module } from '@nestjs/common'; 2 | import { AccessControl } from 'accesscontrol'; 3 | 4 | import { OPTIONS_TOKEN, ROLES_BUILDER_TOKEN } from './constants'; 5 | import { AclModuleOptions } from './interfaces'; 6 | import { AclService } from './service'; 7 | 8 | /** 9 | * The AclModule will do 2 things 10 | * 1. He will check acl depending on user roles 11 | * 2. He will check acl depending on custom rules 12 | */ 13 | @Global() 14 | @Module({}) 15 | export class AclModule { 16 | /** 17 | * Register the AclModule. 18 | */ 19 | static async register(roles: AccessControl, options: AclModuleOptions = {}): Promise { 20 | const rolesBuilderProvider = { 21 | provide: ROLES_BUILDER_TOKEN, 22 | useValue: roles 23 | }; 24 | const optionsProvider = { 25 | provide: OPTIONS_TOKEN, 26 | useValue: options 27 | }; 28 | 29 | return { 30 | module: AclModule, 31 | providers: [rolesBuilderProvider, optionsProvider, AclService], 32 | exports: [rolesBuilderProvider, optionsProvider, AclService] 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, HttpException, Injectable } from '@nestjs/common'; 2 | import { AccessControl } from 'accesscontrol'; 3 | import Debug from 'debug'; 4 | 5 | import { InjectOptions, InjectRolesBuilder } from './decorators'; 6 | import { AclCheckOptions, AclModuleOptions, AclRule, AclRulesCreator } from './interfaces'; 7 | 8 | @Injectable() 9 | export class AclService { 10 | /** 11 | * The log function 12 | */ 13 | protected log = Debug('acl:' + AclService.name); 14 | 15 | /** 16 | * A Map of AclRules 17 | */ 18 | protected rules: Map = new Map(); 19 | 20 | 21 | constructor(@InjectRolesBuilder() protected readonly rolesBuilder: AccessControl, @InjectOptions() protected options: AclModuleOptions){ 22 | this.globalOptions.rejectIfNoRule = options.rejectIfNoRule; 23 | } 24 | 25 | /** 26 | * Get the role builder instance 27 | */ 28 | getRolesBuilder() { 29 | return this.rolesBuilder; 30 | } 31 | 32 | /** 33 | * A key/value pair object of the global options to pass to rule creator during check. 34 | * All rules would be able to read those object keys 35 | */ 36 | protected globalOptions: AclModuleOptions & { [key: string]: any } = {}; 37 | 38 | /** 39 | * Set the global options 40 | * @see globalOptions 41 | */ 42 | setGlobalOptions(globalOptions: AclModuleOptions & { [key: string]: any }) { 43 | this.globalOptions = Object.assign({}, this.options, globalOptions); 44 | } 45 | 46 | /** 47 | * Register an AclRulesCreator 48 | * @param id The id of this rules creator 49 | * @param creator The creator function 50 | */ 51 | registerRules(id: string, creator: AclRulesCreator): AclService { 52 | this.log('Register acl rule %s', id); 53 | this.rules.set(id, creator); 54 | return this; 55 | } 56 | 57 | /** 58 | * This method will check that each rules returned are granted. 59 | * @throws Error|ForbiddenException If access is not granted 60 | */ 61 | async check(options: AclCheckOptions): Promise<{ rule?: AclRule; data?: D }> { 62 | 63 | const opts = { 64 | ...this.globalOptions, 65 | ...options, 66 | rolesBuilder: this.rolesBuilder 67 | }; 68 | const log = this.log.extend(opts.id); 69 | log('Check rule creator'); 70 | const rulesCreator = this.rules.get(opts.id); 71 | let rules: AclRule[] = []; 72 | try { 73 | if (rulesCreator === undefined) { 74 | this.log('No rule creator found'); 75 | if(opts.rejectIfNoRule === true){ 76 | throw new ForbiddenException(`No acl rule creator found for "${opts.id}" context`); 77 | } 78 | return { data: opts.data }; 79 | } 80 | rules = await rulesCreator(opts); 81 | log('Creator returns %d rule(s)', rules.length); 82 | if(rules.length === 0 && opts.rejectIfNoRule === true){ 83 | log('Fail, creator did not return any rule'); 84 | throw new ForbiddenException(`Acl creator did not return any acl rule for "${opts.id}" context`); 85 | } 86 | } catch (e) { 87 | log('Error %o', e); 88 | if (!(e instanceof HttpException)) { 89 | throw new ForbiddenException(e.message); 90 | } 91 | throw e; 92 | } 93 | 94 | const errors: Error[] = []; 95 | let rule: AclRule | undefined; 96 | for (const [index, r] of rules.entries()) { 97 | log('Check rule at index %d', index); 98 | const isValid = await this.isValidRule(r); 99 | if (isValid === true) { 100 | log('Valid rule found at index %s', index); 101 | rule = r; 102 | break; 103 | } else if (isValid instanceof Error) { 104 | log('Invalid rule with custom message error found at index %s %s', index, isValid); 105 | errors.push(isValid); 106 | break; 107 | } 108 | } 109 | 110 | if (rule === undefined) { 111 | log('Fail, creator did not return any valid rule'); 112 | if (errors.length > 0 && options.message === undefined) { 113 | options.message = errors.map((e) => e.message).join(', '); 114 | } 115 | throw new ForbiddenException(options.message); 116 | } 117 | 118 | log('Success'); 119 | 120 | return { rule, data: options.data }; 121 | } 122 | 123 | /** 124 | * Check if an AclRule is valid 125 | */ 126 | protected async isValidRule(rule: AclRule) { 127 | if (rule.req === undefined || !rule.req.granted) { 128 | return false; 129 | } 130 | if (rule.check === undefined) { 131 | return true; 132 | } 133 | const valid = await rule.check(); 134 | if (valid === true) { 135 | return true; 136 | } 137 | return valid; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/test/service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, UnauthorizedException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { AccessControl } from 'accesscontrol'; 4 | 5 | import { AclRulesCreator, AclRulesCreatorOptions } from '../interfaces'; 6 | import { AclModule } from '../module'; 7 | import { AclService } from '../service'; 8 | 9 | export const canDoSomething: AclRulesCreator = (opts: AclRulesCreatorOptions) => { 10 | const { 11 | context: { user }, 12 | rolesBuilder, 13 | data 14 | } = opts; 15 | 16 | if (user === undefined) { 17 | throw new UnauthorizedException(); 18 | } 19 | 20 | return [ 21 | { 22 | req: rolesBuilder.can(user.roles).createAny('Something') 23 | }, 24 | { 25 | req: rolesBuilder.can(user.roles).createOwn('Something'), 26 | check: () => user.id === data.userId 27 | }, 28 | { 29 | req: rolesBuilder.can(user.roles).createOwn('SomethingError'), 30 | check: () => new Error('custom error') 31 | } 32 | ]; 33 | }; 34 | 35 | export const creatorWithNoRule: AclRulesCreator = () => { 36 | return []; 37 | } 38 | 39 | 40 | let mod: TestingModule; 41 | let service: AclService; 42 | const roleBuilder = new AccessControl({ 43 | USER: { 44 | Something: { 45 | 'create:own': ['*'] 46 | } 47 | }, 48 | ADMIN: { 49 | Something: { 50 | 'create:any': ['*'] 51 | } 52 | } 53 | }); 54 | 55 | 56 | 57 | describe('AclService', () => { 58 | beforeAll(async () => { 59 | mod = await Test.createTestingModule({ 60 | imports: [AclModule.register(roleBuilder)] 61 | }).compile(); 62 | service = mod.get(AclService); 63 | service.registerRules('canDoSomething', canDoSomething); 64 | service.registerRules('creatorWithNoRule', creatorWithNoRule); 65 | }); 66 | it('should has a role builder', () => { 67 | const rb = service.getRolesBuilder(); 68 | expect(rb).toBeInstanceOf(AccessControl); 69 | }); 70 | it('should set/get a global option', async () => { 71 | const rb = service.getRolesBuilder(); 72 | service.setGlobalOptions({ 73 | foo: 'bar' 74 | }); 75 | service.registerRules('canReadGlobalOptions', (opts) => { 76 | expect(opts).toHaveProperty('foo'); 77 | expect(opts.foo).toBe('bar'); 78 | return [ 79 | { 80 | req: rb.can('ADMIN').createAny('Something') 81 | } 82 | ]; 83 | }); 84 | const data = { foo: 'bar' }; 85 | const response = await service.check({ 86 | id: 'canReadGlobalOptions', 87 | context: {}, 88 | data 89 | }); 90 | expect(response.data).toEqual(data); 91 | expect(response.rule).toBeDefined(); 92 | }); 93 | describe('Checker', () => { 94 | it('should pass the checker for a missing acl rule', async () => { 95 | const data = { foo: 'bar' }; 96 | const response = await service.check({ 97 | id: 'a_missing_key', 98 | data, 99 | context: { 100 | user: { 101 | roles: ['USER'] 102 | } 103 | } 104 | }); 105 | expect(response.data).toEqual(data); 106 | expect(response.rule).toBeUndefined(); 107 | }); 108 | it('should pass the checker for an existing acl rule', async () => { 109 | const data = { foo: 'bar', userId: 'test' }; 110 | const response = await service.check({ 111 | id: 'canDoSomething', 112 | data, 113 | context: { 114 | user: { 115 | id: 'test', 116 | roles: ['USER'] 117 | } 118 | } 119 | }); 120 | expect(response.data).toEqual(data); 121 | expect(response.rule).toBeDefined(); 122 | }); 123 | it('should pass the checker cause user have access', async () => { 124 | const data = { foo: 'bar', userId: 'test' }; 125 | const response = await service.check({ 126 | id: 'canDoSomething', 127 | data, 128 | context: { 129 | user: { 130 | id: 'test', 131 | roles: ['USER'] 132 | } 133 | } 134 | }); 135 | expect(response.data).toEqual(data); 136 | expect(response.rule).toBeDefined(); 137 | }); 138 | it('should pass the checker cause user have access but is not the owner', async () => { 139 | const data = { foo: 'bar', userId: 'test' }; 140 | const response = await service.check({ 141 | id: 'canDoSomething', 142 | data, 143 | context: { 144 | user: { 145 | id: 'test2', 146 | roles: ['ADMIN'] 147 | } 148 | } 149 | }); 150 | expect(response.data).toEqual(data); 151 | expect(response.rule).toBeDefined(); 152 | }); 153 | it('should not pass the checker cause user does not have access', async () => { 154 | const data = { foo: 'bar', userId: 'test' }; 155 | await expect( 156 | service.check({ 157 | id: 'canDoSomething', 158 | data, 159 | context: { 160 | user: { 161 | id: 'test2', 162 | roles: ['USER'] 163 | } 164 | } 165 | }) 166 | ).rejects.toThrow(ForbiddenException); 167 | }); 168 | it('should not pass the checker cause user is missing', async () => { 169 | const data = { foo: 'bar', userId: 'test' }; 170 | await expect( 171 | service.check({ 172 | id: 'canDoSomething', 173 | data, 174 | context: {} 175 | }) 176 | ).rejects.toThrow(UnauthorizedException); 177 | }); 178 | it('should not pass the checker cause user does not have any compatible role', async () => { 179 | const data = { foo: 'bar', userId: 'test' }; 180 | await expect( 181 | service.check({ 182 | id: 'canDoSomething', 183 | data, 184 | context: { 185 | user: { 186 | id: 'test', 187 | roles: ['DUMMY'] 188 | } 189 | } 190 | }) 191 | ).rejects.toThrow(ForbiddenException); 192 | }); 193 | it('should not pass the checker with a custom error', async () => { 194 | service.registerRules('ruleWithCustomError', () => { 195 | return [ 196 | { 197 | req: service.getRolesBuilder().can('ADMIN').createAny('Something'), 198 | check: () => new Error('Custom error message') 199 | } 200 | ]; 201 | }); 202 | expect.assertions(2); 203 | try { 204 | await service.check({ 205 | id: 'ruleWithCustomError', 206 | context: {} 207 | }); 208 | } catch (e) { 209 | expect(e).toBeInstanceOf(ForbiddenException); 210 | expect(e.message).toContain('Custom error message'); 211 | } 212 | }); 213 | it('should not pass the checker with a custom http error', async () => { 214 | const error = new ForbiddenException('Custom error message'); 215 | service.registerRules('ruleWithCustomError', () => { 216 | return [ 217 | { 218 | req: service.getRolesBuilder().can('ADMIN').createAny('Something'), 219 | check: () => error 220 | } 221 | ]; 222 | }); 223 | expect.assertions(3); 224 | try { 225 | await service.check({ 226 | id: 'ruleWithCustomError', 227 | context: {} 228 | }); 229 | } catch (e) { 230 | expect(e).toBeInstanceOf(ForbiddenException); 231 | expect(e).toStrictEqual(error); 232 | expect(e.message).toContain('Custom error message'); 233 | } 234 | }); 235 | it('should not pass the checker for a missing acl rule creator cause rejectIfNoRule is true', async () => { 236 | const data = { foo: 'bar' }; 237 | await expect(service.check({ 238 | id: 'creatorWithNoRule_rejectIfNoRule', 239 | data, 240 | rejectIfNoRule: true, 241 | context: { 242 | user: { 243 | roles: ['USER'] 244 | } 245 | } 246 | })).rejects.toThrow('No acl rule creator found for "creatorWithNoRule_rejectIfNoRule" context'); 247 | }); 248 | it('should not pass the checker for an acl rule creator returning no rule cause rejectIfNoRule is true', async () => { 249 | const data = { foo: 'bar' }; 250 | await expect(service.check({ 251 | id: 'creatorWithNoRule', 252 | data, 253 | rejectIfNoRule: true, 254 | context: { 255 | user: { 256 | roles: ['USER'] 257 | } 258 | } 259 | })).rejects.toThrow('Acl creator did not return any acl rule for "creatorWithNoRule" context'); 260 | }); 261 | }); 262 | }); 263 | 264 | describe('AclService with options', () => { 265 | beforeAll(async () => { 266 | mod = await Test.createTestingModule({ 267 | imports: [AclModule.register(roleBuilder, {rejectIfNoRule: true})] 268 | }).compile(); 269 | service = mod.get(AclService); 270 | service = mod.get(AclService); 271 | service.registerRules('canDoSomething', canDoSomething); 272 | service.registerRules('creatorWithNoRule_globalOptions', creatorWithNoRule); 273 | }); 274 | describe('Checker', () => { 275 | it('should not pass the checker for a missing acl rule creator cause global option rejectIfNoRule is true', async () => { 276 | const data = { foo: 'bar' }; 277 | await expect(service.check({ 278 | id: 'creatorWithNoRule_rejectIfNoRule_globalOptions', 279 | data, 280 | context: { 281 | user: { 282 | roles: ['USER'] 283 | } 284 | } 285 | })).rejects.toThrow('No acl rule creator found for "creatorWithNoRule_rejectIfNoRule_globalOptions" context'); 286 | }); 287 | it('should not pass the checker for an acl rule creator returning no rule cause global option rejectIfNoRule is true', async () => { 288 | const data = { foo: 'bar' }; 289 | await expect(service.check({ 290 | id: 'creatorWithNoRule_globalOptions', 291 | data, 292 | context: { 293 | user: { 294 | roles: ['USER'] 295 | } 296 | } 297 | })).rejects.toThrow('Acl creator did not return any acl rule for "creatorWithNoRule_globalOptions" context'); 298 | }); 299 | }); 300 | }) 301 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "declarationMap": true, 6 | "noImplicitAny": false, 7 | "noUnusedLocals": false, 8 | "removeComments": true, 9 | "noLib": false, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": true, 14 | "allowJs": true, 15 | "outDir": "dist", 16 | "lib": ["es7"], 17 | "strictNullChecks": true 18 | }, 19 | "include": ["src/**/*"] 20 | } 21 | -------------------------------------------------------------------------------- /typedoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | out: 'docs', 3 | entryPoints: ['src/index.ts'], 4 | exclude: ['**/test/**', '**/apollo-server-*/**'], 5 | theme: 'default', 6 | name: 'Nestjs ACL Documentation', 7 | excludeExternals: false, 8 | excludePrivate: false, 9 | hideGenerator: true 10 | }; 11 | --------------------------------------------------------------------------------