├── .cz-config.js
├── .eslintignore
├── .eslintrc.js
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── pull_request_template.md
├── stale.yml
└── workflows
│ ├── main.yaml
│ ├── release.yaml
│ └── sync-docs.yaml
├── .gitignore
├── .husky
├── commit-msg
├── pre-commit
└── prepare-commit-msg
├── .mocharc.json
├── .npmrc
├── .nycrc
├── .prettierignore
├── .prettierrc
├── .sonarcloud.properties
├── .vscode
├── settings.json
└── tasks.json
├── .yo-rc.json
├── CHANGELOG.md
├── DEVELOPING.md
├── LICENSE
├── README.md
├── catalog-info.yaml
├── commitlint.config.js
├── docs
└── README.md
├── mkdocs.yml
├── package-lock.json
├── package.json
├── src
├── __tests__
│ ├── acceptance
│ │ └── README.md
│ ├── integration
│ │ └── README.md
│ └── unit
│ │ ├── README.md
│ │ ├── authorization-action.provider.unit.ts
│ │ ├── authorization-metadata.provider.unit.ts
│ │ ├── casbin-authorization-action.provider.unit.ts
│ │ └── data
│ │ ├── index.ts
│ │ ├── mock-model.ts
│ │ ├── mock-policy.ts
│ │ ├── mock-user.ts
│ │ └── permission.enum.ts
├── component.ts
├── controllers
│ └── README.md
├── decorators
│ ├── README.md
│ ├── authorize.decorator.ts
│ ├── index.ts
│ └── spec-preprocessor.ts
├── enhancer
│ └── spec-description-enhancer.ts
├── error-keys.ts
├── index.ts
├── keys.ts
├── mixins
│ └── README.md
├── policy.csv
├── providers
│ ├── README.md
│ ├── authorization-action.provider.ts
│ ├── authorization-metadata.provider.ts
│ ├── casbin-authorization-action.provider.ts
│ ├── casbin-enforcer-config.provider.ts
│ ├── index.ts
│ └── user-permissions.provider.ts
├── release_notes
│ ├── mymarkdown.ejs
│ ├── post-processing.js
│ └── release-notes.js
├── repositories
│ └── README.md
└── types.ts
└── tsconfig.json
/.cz-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | types: [
3 | {value: 'feat', name: 'feat: A new feature'},
4 | {value: 'fix', name: 'fix: A bug fix'},
5 | {value: 'docs', name: 'docs: Documentation only changes'},
6 | {
7 | value: 'style',
8 | name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)',
9 | },
10 | {
11 | value: 'refactor',
12 | name: 'refactor: A code change that neither fixes a bug nor adds a feature',
13 | },
14 | {
15 | value: 'perf',
16 | name: 'perf: A code change that improves performance',
17 | },
18 | {value: 'test', name: 'test: Adding missing tests'},
19 | {
20 | value: 'chore',
21 | name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation',
22 | },
23 | {value: 'revert', name: 'revert: Reverting a commit'},
24 | {value: 'WIP', name: 'WIP: Work in progress'},
25 | ],
26 |
27 | scopes: [
28 | {name: 'chore'},
29 | {name: 'deps'},
30 | {name: 'ci-cd'},
31 | {name: 'component'},
32 | {name: 'provider'},
33 | {name: 'core'},
34 | {name: 'maintenance'},
35 | ],
36 |
37 | appendBranchNameToCommitMessage: true,
38 | appendIssueFromBranchName: true,
39 | allowTicketNumber: false,
40 | isTicketNumberRequired: false,
41 |
42 | // override the messages, defaults are as follows
43 | messages: {
44 | type: "Select the type of change that you're committing:",
45 | scope: 'Denote the SCOPE of this change:',
46 | // used if allowCustomScopes is true
47 | customScope: 'Denote the SCOPE of this change:',
48 | subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n',
49 | body: 'Provide a LONGER description of the change (mandatory). Use "\\n" to break new line:\n',
50 | breaking: 'List any BREAKING CHANGES (optional):\n',
51 | footer: 'List any ISSUES CLOSED by this change (optional). E.g.: GH-144:\n',
52 | confirmCommit: 'Are you sure you want to proceed with the commit above?',
53 | },
54 |
55 | allowCustomScopes: false,
56 | allowBreakingChanges: ['feat', 'fix'],
57 |
58 | // limit subject length
59 | subjectLimit: 100,
60 | breaklineChar: '|', // It is supported for fields body and footer.
61 | footerPrefix: '',
62 | askForBreakingChangeFirst: true, // default is false
63 | };
64 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | coverage/
4 | api-docs/
5 | .cz-config.js
6 | commitlint.config.js
7 | .nyc_output
8 | .eslintrc.js
9 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: '@loopback/eslint-config',
3 | rules: {
4 | 'no-extra-boolean-cast': 'off',
5 | '@typescript-eslint/interface-name-prefix': 'off',
6 | 'no-prototype-builtins': 'off',
7 | 'no-unused-vars': 'off',
8 | },
9 | parserOptions: {
10 | project: './tsconfig.json',
11 | tsconfigRootDir: __dirname,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at support@sourcefuse.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # loopback4-authentication
2 |
3 | ## Contributing
4 |
5 | First off, thank you for considering contributing to the project. It's people like you that helps in keeping this extension useful.
6 |
7 | ### Where do I go from here?
8 |
9 | If you've noticed a bug or have a question, [search the issue tracker](https://github.com/sourcefuse/loopback4-authentication/issues) to see if
10 | someone else in the community has already created a ticket. If not, go ahead and
11 | [make one](https://github.com/sourcefuse/loopback4-authentication/issues/new/choose)!
12 |
13 | ### Fork & create a branch
14 |
15 | If this is something you think you can fix, then [fork](https://help.github.com/articles/fork-a-repo) this repo and
16 | create a branch with a descriptive name.
17 |
18 | A good branch name would be (where issue #325 is the ticket you're working on):
19 |
20 | ```sh
21 | git checkout -b 325-add-new-feature
22 | ```
23 |
24 | ### Make a Pull Request
25 |
26 | At this point, you should switch back to your master branch and make sure it's
27 | up to date with loopback4-authentication's master branch:
28 |
29 | ```sh
30 | git remote add upstream git@github.com:sourcefuse/loopback4-authentication.git
31 | git checkout master
32 | git pull upstream master
33 | ```
34 |
35 | Then update your feature branch from your local copy of master, and push it!
36 |
37 | ```sh
38 | git checkout 325-add-new-feature
39 | git rebase master
40 | git push --set-upstream origin 325-add-new-feature
41 | ```
42 |
43 | Finally, go to GitHub and [make a Pull Request](https://help.github.com/articles/creating-a-pull-request).
44 |
45 | ### Keeping your Pull Request updated
46 |
47 | If a maintainer asks you to "rebase" your PR, they're saying that a lot of code
48 | has changed, and that you need to update your branch so it's easier to merge.
49 |
50 | To learn more about rebasing in Git, there are a lot of [good][git rebasing]
51 | [resources][interactive rebase] but here's the suggested workflow:
52 |
53 | ```sh
54 | git checkout 325-add-new-feature
55 | git pull --rebase upstream master
56 | git push --force-with-lease 325-add-new-feature
57 | ```
58 |
59 | [git rebasing]: http://git-scm.com/book/en/Git-Branching-Rebasing
60 | [interactive rebase]: https://help.github.com/articles/interactive-rebase
61 |
--------------------------------------------------------------------------------
/.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 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
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 | **Additional context**
27 | Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/.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/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
4 |
5 | Fixes # (issue)
6 |
7 | ## Type of change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] Intermediate change (work in progress)
15 |
16 | ## How Has This Been Tested?
17 |
18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
19 |
20 | - [ ] Test A
21 | - [ ] Test B
22 |
23 | ## Checklist:
24 |
25 | - [ ] Performed a self-review of my own code
26 | - [ ] npm test passes on your machine
27 | - [ ] New tests added or existing tests modified to cover all changes
28 | - [ ] Code conforms with the style guide
29 | - [ ] API Documentation in code was updated
30 | - [ ] Any dependent changes have been merged and published in downstream modules
31 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Label to use when marking an issue or a PR as stale
2 | staleLabel: stale
3 |
4 | # Configuration for issues
5 | issues:
6 | # Number of days of inactivity before an issue becomes stale
7 | daysUntilStale: 90
8 | # Comment to post when marking an issue as stale. Set to `false` to disable
9 | markComment: >
10 | This issue has been marked stale because it has not seen any activity within
11 | three months. If you believe this to be an error, please contact one of the code owners.
12 | This issue will be closed within 15 days of being stale.
13 | # Number of days of inactivity before a stale issue is closed
14 | daysUntilClose: 15
15 | # Comment to post when closing a stale issue. Set to `false` to disable
16 | closeComment: >
17 | This issue has been closed due to continued inactivity. Thank you for your understanding.
18 | If you believe this to be in error, please contact one of the code owners.
19 | # Configuration for pull requests
20 | pulls:
21 | # Number of days of inactivity before a PR becomes stale
22 | daysUntilStale: 60
23 | # Comment to post when marking a PR as stale. Set to `false` to disable
24 | markComment: >
25 | This pull request has been marked stale because it has not seen any activity
26 | within two months. It will be closed within 15 days of being stale
27 | unless there is new activity.
28 | # Number of days of inactivity before a stale PR is closed
29 | daysUntilClose: 15
30 | # Comment to post when closing a stale issue. Set to `false` to disable
31 | closeComment: >
32 | This pull request has been closed due to continued inactivity. If you are
33 | interested in finishing the proposed changes, then feel free to re-open
34 | this pull request or open a new one.
35 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | # This workflow contains a single job called "npm_test"
10 | jobs:
11 | npm_test:
12 | # The type of runner that the job will run on
13 | runs-on: ubuntu-latest
14 |
15 | # Steps represent a sequence of tasks that will be executed as part of the job
16 | steps:
17 | # Checks-out your repository under $GITHUB_WORKSPACE
18 | - uses: actions/checkout@v3
19 | - uses: actions/setup-node@v3
20 | with:
21 | node-version: '18.x'
22 |
23 | - name: Install Dependencies 📌
24 | run: npm ci
25 |
26 | - name: Run Test Cases 🔧
27 | run: npm run test
28 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | # This Manually Executable Workflow is for NPM Releases
2 |
3 | name: Release [Manual]
4 | on: workflow_dispatch
5 | permissions:
6 | contents: write
7 | jobs:
8 | Release:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | with:
13 | # fetch-depth is necessary to get all tags
14 | # otherwise lerna can't detect the changes and will end up bumping the versions for all packages
15 | fetch-depth: 0
16 | token: ${{ secrets.RELEASE_COMMIT_GH_PAT }}
17 | - name: Setup Node
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: '18.x'
21 | - name: Configure CI Git User
22 | run: |
23 | git config --global user.name $CONFIG_USERNAME
24 | git config --global user.email $CONFIG_EMAIL
25 | git remote set-url origin https://$GITHUB_ACTOR:$GITHUB_PAT@github.com/sourcefuse/loopback4-authorization
26 | env:
27 | GITHUB_PAT: ${{ secrets.RELEASE_COMMIT_GH_PAT }}
28 | CONFIG_USERNAME: ${{ vars.RELEASE_COMMIT_USERNAME }}
29 | CONFIG_EMAIL: ${{ vars.RELEASE_COMMIT_EMAIL }}
30 | - name: Authenticate with Registry
31 | run: |
32 | echo "@${NPM_USERNAME}:registry=https://registry.npmjs.org/" > .npmrc
33 | echo "registry=https://registry.npmjs.org/" >> .npmrc
34 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
35 | npm whoami
36 | env:
37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
38 | NPM_USERNAME: ${{ vars.NPM_USERNAME }}
39 |
40 | - name: Install 📌
41 | run: |
42 | npm install
43 | - name: Test 🔧
44 | run: npm run test
45 | - name: Semantic Publish to NPM 🚀
46 | # "HUSKY=0" disables pre-commit-msg check (Needed in order to allow semantic-release perform the release commit)
47 | run: HUSKY=0 npx semantic-release
48 | env:
49 | GH_TOKEN: ${{ secrets.RELEASE_COMMIT_GH_PAT }}
50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
51 | - name: Changelog 📝
52 | run: cd src/release_notes && HUSKY=0 node release-notes.js
53 |
--------------------------------------------------------------------------------
/.github/workflows/sync-docs.yaml:
--------------------------------------------------------------------------------
1 | name: Sync Docs to arc-docs repo
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | env:
9 | DOCS_REPO: sourcefuse/arc-docs
10 | BRANCH_PREFIX: automated-docs-sync/
11 | GITHUB_TOKEN: ${{secrets.ARC_DOCS_API_TOKEN_GITHUB}}
12 | CONFIG_USERNAME: ${{ vars.DOCS_PR_USERNAME }}
13 | CONFIG_EMAIL: ${{ vars.DOCS_PR_EMAIL }}
14 |
15 | jobs:
16 | sync-docs:
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout Extension Code
21 | uses: actions/checkout@v3
22 | with:
23 | token: ${{env.GITHUB_TOKEN}}
24 | path: './extension/'
25 |
26 | - name: Checkout Docs Repository
27 | uses: actions/checkout@v3
28 | with:
29 | token: ${{env.GITHUB_TOKEN}}
30 | repository: ${{env.DOCS_REPO}}
31 | path: './arc-docs/'
32 |
33 | - name: Configure GIT
34 | id: configure_git
35 | working-directory: arc-docs
36 | run: |
37 | git config --global user.email $CONFIG_EMAIL
38 | git config --global user.name $CONFIG_USERNAME
39 |
40 | extension_branch="${{env.BRANCH_PREFIX}}$(basename $GITHUB_REPOSITORY)"
41 | echo "extension_branch=$extension_branch" >> $GITHUB_OUTPUT
42 |
43 | - name: Update Files
44 | id: update_files
45 | working-directory: arc-docs
46 | run: |
47 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}"
48 |
49 | # Create a new branch if it doesn't exist, or switch to it if it does
50 | git checkout -B $extension_branch || git checkout $extension_branch
51 |
52 | # Copy README from the extension repo
53 | cp ../extension/docs/README.md docs/arc-api-docs/extensions/$(basename $GITHUB_REPOSITORY)/
54 | git add .
55 |
56 | if git diff --quiet --cached; then
57 | have_changes="false";
58 | else
59 | have_changes="true";
60 | fi
61 |
62 | echo "Have Changes to be commited: $have_changes"
63 | echo "have_changes=$have_changes" >> $GITHUB_OUTPUT
64 |
65 | - name: Commit Changes
66 | id: commit
67 | working-directory: arc-docs
68 | if: steps.update_files.outputs.have_changes == 'true'
69 | run: |
70 | git commit -m "sync $(basename $GITHUB_REPOSITORY) docs"
71 | - name: Push Changes
72 | id: push_branch
73 | if: steps.update_files.outputs.have_changes == 'true'
74 | working-directory: arc-docs
75 | run: |
76 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}"
77 | git push https://oauth2:${GITHUB_TOKEN}@github.com/${{env.DOCS_REPO}}.git HEAD:$extension_branch --force
78 |
79 | - name: Check PR Status
80 | id: pr_status
81 | if: steps.update_files.outputs.have_changes == 'true'
82 | working-directory: arc-docs
83 | run: |
84 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}"
85 | gh pr status --json headRefName >> "${{github.workspace}}/pr-status.json"
86 | pr_exists="$(jq --arg extension_branch "$extension_branch" '.createdBy[].headRefName == $extension_branch' "${{github.workspace}}/pr-status.json")"
87 | echo "PR Exists: $pr_exists"
88 | echo "pr_exists=$pr_exists" >> $GITHUB_OUTPUT
89 |
90 | - name: Create Pull Request
91 | id: create_pull_request
92 | if: steps.pr_status.outputs.pr_exists != 'true' && steps.update_files.outputs.have_changes == 'true'
93 | working-directory: arc-docs
94 | run: |
95 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}"
96 |
97 | gh pr create --head $(git branch --show-current) --title "Sync ${{ github.event.repository.name }} Docs" --body "This Pull Request has been created by the 'sync-docs' action within the '${{ github.event.repository.name }}' repository, with the purpose of updating markdown files."
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # Generated apidocs
61 | api-docs/
62 |
63 | # Transpiled JavaScript files from Typescript
64 | dist/
65 |
66 | *.tsbuildinfo
67 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm test
5 |
--------------------------------------------------------------------------------
/.husky/prepare-commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | exec < /dev/tty && npx cz --hook || true
5 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "recursive": true,
3 | "require": "source-map-support/register"
4 | }
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=true
2 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["dist"],
3 | "exclude": ["dist/__tests__/"],
4 | "extension": [".js", ".ts"],
5 | "reporter": ["text", "html"],
6 | "exclude-after-remap": false
7 | }
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | *.json
3 | api-docs
4 | coverage
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": false,
3 | "singleQuote": true,
4 | "printWidth": 80,
5 | "trailingComma": "all",
6 | "arrowParens": "avoid"
7 | }
8 |
--------------------------------------------------------------------------------
/.sonarcloud.properties:
--------------------------------------------------------------------------------
1 | # Path to sources
2 | sonar.sources=src
3 | sonar.exclusions=src/__tests__/**
4 | #sonar.inclusions=
5 |
6 | # Path to tests
7 | sonar.tests=src/__tests__
8 | #sonar.test.exclusions=
9 | #sonar.test.inclusions=
10 |
11 | # Source encoding
12 | sonar.sourceEncoding=UTF-8
13 |
14 | # Exclusions for copy-paste detection
15 | #sonar.cpd.exclusions=
16 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.rulers": [80],
3 | "editor.tabCompletion": "on",
4 | "editor.tabSize": 2,
5 | "editor.trimAutoWhitespace": true,
6 | "editor.formatOnSave": true,
7 |
8 | "sonarlint.connectedMode.project": {
9 | "serverId": "sf_sonar",
10 | "projectKey": "sourcefuse_loopback4-authorization"
11 | },
12 |
13 | "files.exclude": {
14 | "**/.DS_Store": true,
15 | "**/.git": true,
16 | "**/.hg": true,
17 | "**/.svn": true,
18 | "**/CVS": true,
19 | "dist": true,
20 | },
21 | "files.insertFinalNewline": true,
22 | "files.trimTrailingWhitespace": true,
23 |
24 | "typescript.tsdk": "./node_modules/typescript/lib"
25 | }
26 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Watch and Compile Project",
8 | "type": "shell",
9 | "command": "npm",
10 | "args": ["--silent", "run", "build:watch"],
11 | "group": {
12 | "kind": "build",
13 | "isDefault": true
14 | },
15 | "problemMatcher": "$tsc-watch"
16 | },
17 | {
18 | "label": "Build, Test and Lint",
19 | "type": "shell",
20 | "command": "npm",
21 | "args": ["--silent", "run", "test:dev"],
22 | "group": {
23 | "kind": "test",
24 | "isDefault": true
25 | },
26 | "problemMatcher": ["$tsc", "$tslint5"]
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Release [v7.0.3](https://github.com/sourcefuse/loopback4-authorization/compare/v7.0.2..v7.0.3) December 18, 2024
2 | Welcome to the December 18, 2024 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
3 |
4 | - [](https://github.com/sourcefuse/loopback4-authorization/issues/) :- [chore(deps): version update ](https://github.com/sourcefuse/loopback4-authorization/commit/5d0714562da3bf702dc3170d8b574bf1bd613de8) was commited on December 18, 2024 by [Sunny Tyagi](mailto:107617248+Tyagi-Sunny@users.noreply.github.com)
5 |
6 | - * chore(deps): version update
7 |
8 | - version update
9 |
10 | - gh-0
11 |
12 | - * chore(deps): include pr 141
13 |
14 | - include pr 141
15 |
16 | - 141
17 |
18 |
19 | Clink on the above links to understand the changes in detail.
20 | ___
21 |
22 | ## Release [v7.0.2](https://github.com/sourcefuse/loopback4-authorization/compare/v7.0.1..v7.0.2) June 4, 2024
23 | Welcome to the June 4, 2024 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
24 |
25 | - [loopback version updates](https://github.com/sourcefuse/loopback4-authorization/issues/130) :- [chore(deps): loopback version updates ](https://github.com/sourcefuse/loopback4-authorization/commit/741835854eaeb59849d86a79213dd9373135c686) was commited on June 4, 2024 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com)
26 |
27 | - loopback version updates
28 |
29 | - GH-130
30 |
31 |
32 | Clink on the above links to understand the changes in detail.
33 | ___
34 |
35 | ## Release [v7.0.1](https://github.com/sourcefuse/loopback4-authorization/compare/v7.0.0..v7.0.1) May 15, 2024
36 | Welcome to the May 15, 2024 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
37 |
38 | - [Changelog not generated](https://github.com/sourcefuse/loopback4-authorization/issues/128) :- [fix(ci-cd): step to generate changelog ](https://github.com/sourcefuse/loopback4-authorization/commit/341f8154ed9fdb306cba02e8f392c91963012cd6) was commited on May 15, 2024 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com)
39 |
40 | - add the missing ci checks on pr
41 |
42 | - GH-128
43 |
44 |
45 | - [Copy the Readme to root as well](https://github.com/sourcefuse/loopback4-authorization/issues/126) :- [docs(chore): copy the Readme to root as well ](https://github.com/sourcefuse/loopback4-authorization/commit/0fc85a206dfb4a3e6a9c48148165a60d90f31fe5) was commited on May 13, 2024 by [arpit1503khanna](mailto:108673359+arpit1503khanna@users.noreply.github.com)
46 |
47 | - copy the Readme to root as well
48 |
49 | - GH-126
50 |
51 |
52 | - [](https://github.com/sourcefuse/loopback4-authorization/issues/) :- [](https://github.com/sourcefuse/loopback4-authorization/commit/964d5c67422b68ed1f98054cab1f1b2b0ee63265) was commited on March 13, 2024 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com)
53 |
54 |
55 | - [](https://github.com/sourcefuse/loopback4-authorization/issues/00) :- [chore(ci-cd): update the readme path in sync docs workflow ](https://github.com/sourcefuse/loopback4-authorization/commit/d1008920f8cd1cb3ad58964ee00536d57f09b55a) was commited on March 13, 2024 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com)
56 |
57 | - readme moved to docs folder
58 |
59 | - GH-00
60 |
61 |
62 | Clink on the above links to understand the changes in detail.
63 | ___
64 |
65 | ## Release [v6.1.0](https://github.com/sourcefuse/loopback4-authorization/compare/v6.0.0..v6.1.0) July 14, 2023
66 | Welcome to the July 14, 2023 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
67 |
68 | - [Add ARC Branding in README](https://github.com/sourcefuse/loopback4-authorization/issues/99) :- [docs(chore): add arc branding ](https://github.com/sourcefuse/loopback4-authorization/commit/aa5b1dc8f48ba02a8a3a32ba1ce8fd3a923aa0ab) was commited on July 14, 2023 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com)
69 |
70 | - add arc branding
71 |
72 | - GH-99
73 |
74 |
75 | - [Loopback version update](https://github.com/sourcefuse/loopback4-authorization/issues/87) :- [chore(deps): loopback version updates ](https://github.com/sourcefuse/loopback4-authorization/commit/05d2fed8347491f4c5238b0a3d582da5d77d1e55) was commited on July 14, 2023 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com)
76 |
77 | - loopback version updates
78 |
79 | - GH-87
80 |
81 |
82 | - [loopback4-authorization:add ability to override permissions passed in metadata](https://github.com/sourcefuse/loopback4-authorization/issues/95) :- [feat(provider): add ability to override metadata permissions ](https://github.com/sourcefuse/loopback4-authorization/commit/7fd0f937960f7d8822b8d58efb0409fab054c731) was commited on July 10, 2023 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com)
83 |
84 | - * feat(provider): add ability to override metadata permissions
85 |
86 | - add ability to override metadata permissions with the permission object passed
87 |
88 | - by the user.
89 |
90 | - GH-95
91 |
92 | - * feat(provider): add ability to override user permissions
93 |
94 | - add ability to provide user permissions
95 |
96 | - GH-95
97 |
98 | - * docs(provider): add description to overrride the permissions in readme file
99 |
100 | - add description to overrride the permissions in readme file
101 |
102 | - GH-95
103 |
104 |
105 | - [Throws not found for static files](https://github.com/sourcefuse/loopback4-authorization/issues/51) :- [docs(component): update readme for serving static files ](https://github.com/sourcefuse/loopback4-authorization/commit/b57177ec180a60c0c59aa0189cb4240751cadb0b) was commited on July 6, 2023 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com)
106 |
107 | - update readme for serving static files
108 |
109 | - GH-51
110 |
111 |
112 | - [](https://github.com/sourcefuse/loopback4-authorization/issues/) :- [fix(chore): fix sonar code smells ](https://github.com/sourcefuse/loopback4-authorization/commit/7c6fc277b7649131913a40c45985b87890093be3) was commited on July 6, 2023 by [RaghavaroraSF](mailto:97958393+RaghavaroraSF@users.noreply.github.com)
113 |
114 | - * fix(chore): fix sonar code smells
115 |
116 | - resolving sonar code smells to improve quality gate
117 |
118 | - GH-78
119 |
120 | - * fix(chore): fix sonar code smells
121 |
122 | - Resolving sonar code smells to improve quality gate
123 |
124 | - GH-78
125 |
126 | - * refactor(chore): fix sonar code smells
127 |
128 | - and rebase
129 |
130 | - GH-78
131 |
132 | - ---------
133 |
134 | - Co-authored-by: Shubham P <shubham.prajapat@sourcefuse.com>
135 |
136 |
137 | - [](https://github.com/sourcefuse/loopback4-authorization/issues/0) :- [refactor(maintenance): remove tracking dist folder on git ](https://github.com/sourcefuse/loopback4-authorization/commit/c4223d0e5fbdefdcacbc4c7fe6428a1c9f10d698) was commited on July 6, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com)
138 |
139 | - it's not needed and it just contained build files from last year
140 |
141 | - GH-0
142 |
143 |
144 | Clink on the above links to understand the changes in detail.
145 | ___
146 |
147 | ## Release [v6.0.0](https://github.com/sourcefuse/loopback4-authorization/compare/v5.1.3..v6.0.0) June 7, 2023
148 | Welcome to the June 7, 2023 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
149 |
150 | - [Loopback version update](https://github.com/sourcefuse/loopback4-authorization/issues/87) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-authorization/commit/1dbb12cbb7fa2f86ec2f96dd0a36551b46581230) was commited on June 1, 2023 by [RaghavaroraSF](mailto:97958393+RaghavaroraSF@users.noreply.github.com)
151 |
152 | - Loopback version update
153 |
154 | - GH-87
155 |
156 |
157 | - [Remove support for node v14](https://github.com/sourcefuse/loopback4-authorization/issues/91) :- [feat(chore): remove support for node v14,v12,v10 ](https://github.com/sourcefuse/loopback4-authorization/commit/9e526e59935ebc0e3c6344dcd078840e017814fd) was commited on May 11, 2023 by [RaghavaroraSF](mailto:97958393+RaghavaroraSF@users.noreply.github.com)
158 |
159 | - Version 14, v12, v10 of Nodejs reached its end of life
160 |
161 | - BREAKING CHANGE:
162 |
163 | - End of life of node v14, node v12 and node v10
164 |
165 | - GH-91
166 |
167 |
168 | - [License and Copyright Headers’ Year Upgradation](https://github.com/sourcefuse/loopback4-authorization/issues/89) :- [chore(maintenance): update license and copyright headers ](https://github.com/sourcefuse/loopback4-authorization/commit/768f2868a08129e4f8615c7ef7c61bf0144a709f) was commited on May 4, 2023 by [RaghavaroraSF](mailto:97958393+RaghavaroraSF@users.noreply.github.com)
169 |
170 | - to reflect the year change
171 |
172 | - GH-89
173 |
174 |
175 | Clink on the above links to understand the changes in detail.
176 | ___
177 |
178 | ## Release [v5.1.3](https://github.com/sourcefuse/loopback4-authorization/compare/v5.1.2..v5.1.3) April 24, 2023
179 | Welcome to the April 24, 2023 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
180 |
181 | - [Loopback version update](https://github.com/sourcefuse/loopback4-authorization/issues/87) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-authorization/commit/c36448c4412a849f25e8e24d7d6fd392a61829ed) was commited on April 24, 2023 by [RaghavaroraSF](mailto:97958393+RaghavaroraSF@users.noreply.github.com)
182 |
183 | - loopback version update
184 |
185 | - GH-87
186 |
187 |
188 | Clink on the above links to understand the changes in detail.
189 | ___
190 |
191 | ## Release [v5.1.2](https://github.com/sourcefuse/loopback4-authorization/compare/v5.1.1..v5.1.2) March 13, 2023
192 | Welcome to the March 13, 2023 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
193 |
194 | - [loopback version update](https://github.com/sourcefuse/loopback4-authorization/issues/83) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-authorization/commit/820244541cc590d69588484083fb5e6c31074040) was commited on March 13, 2023 by [Gautam Agarwal](mailto:108651274+gautam23-sf@users.noreply.github.com)
195 |
196 | - loopback version update
197 |
198 | - GH-83
199 |
200 |
201 | - [Stale Bot missing in the repository](https://github.com/sourcefuse/loopback4-authorization/issues/81) :- [chore(chore): add github stale bot ](https://github.com/sourcefuse/loopback4-authorization/commit/b216e4f111256fdb1a003ac35a35eaffb80d9907) was commited on February 27, 2023 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com)
202 |
203 | - Added stale.yml file to configure stale options
204 |
205 | - GH-81
206 |
207 |
208 | Clink on the above links to understand the changes in detail.
209 | ___
210 |
211 | ## Release [v5.1.1](https://github.com/sourcefuse/loopback4-authorization/compare/v5.1.0..v5.1.1) February 17, 2023
212 | Welcome to the February 17, 2023 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
213 |
214 | - [Correct the changelog Format](https://github.com/sourcefuse/loopback4-authorization/issues/79) :- [fix(chore): correct the changelog format ](https://github.com/sourcefuse/loopback4-authorization/commit/edc06409cf17bd74157bf2b8e4c350b0b6a03e53) was commited on February 17, 2023 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com)
215 |
216 | - now issue description will be visible
217 |
218 | - GH-79
219 |
220 |
221 | - [Package Update : loopback4-authorization](https://github.com/sourcefuse/loopback4-authorization/issues/76) :- [fix(chore): remove all current vulnerability of loopback4-authorization ](https://github.com/sourcefuse/loopback4-authorization/commit/2da3301b1922c1ec0cc61ff6130c4b34aa84f7da) was commited on February 17, 2023 by [Sunny Tyagi](mailto:107617248+Tyagi-Sunny@users.noreply.github.com)
222 |
223 | - remove all current vulnerability of loopback4-authorization
224 |
225 | - GH-76
226 |
227 |
228 | Clink on the above links to understand the changes in detail.
229 | ___
230 |
231 | ## Release [v5.1.0](https://github.com/sourcefuse/loopback4-authorization/compare/v5.0.9..v5.1.0) January 11, 2023
232 | Welcome to the January 11, 2023 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
233 |
234 | - [](https://github.com/sourcefuse/loopback4-authorization/issues/-70) :- [feat(chore): generate detailed and informative changelog ](https://github.com/sourcefuse/loopback4-authorization/commit/c69eaf575c6b21a23779818c27ad3a4e749aaf6a) was commited on January 10, 2023 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com)
235 |
236 | - Using Customizable npm package to generate changelog
237 |
238 | - GH-70
239 |
240 |
241 | - [](https://github.com/sourcefuse/loopback4-authorization/issues/-68) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-authorization/commit/bab60712fa528f2b918106a384f121a5a832444d) was commited on January 10, 2023 by [Surbhi Sharma](mailto:98279679+Surbhi-sharma1@users.noreply.github.com)
242 |
243 | - Updated version of the lb4 dependencies to the latest.
244 |
245 | - GH-68
246 |
247 |
248 | Clink on the above links to understand the changes in detail.
249 | ___
250 |
251 | ## [5.0.9](https://github.com/sourcefuse/loopback4-authorization/compare/v5.0.8...v5.0.9) (2022-12-05)
252 |
253 | ## [5.0.8](https://github.com/sourcefuse/loopback4-authorization/compare/v5.0.7...v5.0.8) (2022-10-31)
254 |
255 | ## [5.0.7](https://github.com/sourcefuse/loopback4-authorization/compare/v5.0.6...v5.0.7) (2022-09-08)
256 |
257 | ## [5.0.6](https://github.com/sourcefuse/loopback4-authorization/compare/v5.0.5...v5.0.6) (2022-06-17)
258 |
259 | ## [5.0.5](https://github.com/sourcefuse/loopback4-authorization/compare/v5.0.4...v5.0.5) (2022-05-30)
260 |
261 |
262 | ### Bug Fixes
263 |
264 | * **core:** remove spec enhancer by default ([#53](https://github.com/sourcefuse/loopback4-authorization/issues/53)) ([b76cfb8](https://github.com/sourcefuse/loopback4-authorization/commit/b76cfb8aa9ecf73c24fe9f003ff1d2896977c759)), closes [#0](https://github.com/sourcefuse/loopback4-authorization/issues/0)
265 | * **provider:** change order of not found check ([#52](https://github.com/sourcefuse/loopback4-authorization/issues/52)) ([3213f6d](https://github.com/sourcefuse/loopback4-authorization/commit/3213f6d4ceab7a2d5d2dc236561dbda3de241ec5)), closes [#51](https://github.com/sourcefuse/loopback4-authorization/issues/51)
266 |
267 | ## [5.0.4](https://github.com/sourcefuse/loopback4-authorization/compare/v5.0.3...v5.0.4) (2022-05-25)
268 |
269 |
270 | ### Bug Fixes
271 |
272 | * **component): fix(component:** fix authorize permission missing in path description ([#41](https://github.com/sourcefuse/loopback4-authorization/issues/41)) ([3df07ce](https://github.com/sourcefuse/loopback4-authorization/commit/3df07ce58ca6d9bc74b048fc29f636d772bc2787)), closes [#40](https://github.com/sourcefuse/loopback4-authorization/issues/40)
273 | * **deps:** remove vulnerabilities ([#47](https://github.com/sourcefuse/loopback4-authorization/issues/47)) ([b62dc5a](https://github.com/sourcefuse/loopback4-authorization/commit/b62dc5ad72485795cd814d37f6588946e2faf1f0)), closes [#46](https://github.com/sourcefuse/loopback4-authorization/issues/46) [#46](https://github.com/sourcefuse/loopback4-authorization/issues/46)
274 | * **provider): fix(provider:** authorization action gives 404 for route not found ([#36](https://github.com/sourcefuse/loopback4-authorization/issues/36)) ([c517c0a](https://github.com/sourcefuse/loopback4-authorization/commit/c517c0adbe156f0ad2389bacce6d7e3de3aeaab4)), closes [#35](https://github.com/sourcefuse/loopback4-authorization/issues/35)
--------------------------------------------------------------------------------
/DEVELOPING.md:
--------------------------------------------------------------------------------
1 | # Developer's Guide
2 |
3 | We use Visual Studio Code for developing LoopBack and recommend the same to our
4 | users.
5 |
6 | ## VSCode setup
7 |
8 | Install the following extensions:
9 |
10 | - [tslint](https://marketplace.visualstudio.com/items?itemName=eg2.tslint)
11 | - [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
12 |
13 | ## Setup Commit Hooks
14 |
15 | Run the following script to prepare husky after the first time install -
16 |
17 | `npm run prepare`
18 |
19 | ## Development workflow
20 |
21 | ### Visual Studio Code
22 |
23 | 1. Start the build task (Cmd+Shift+B) to run TypeScript compiler in the
24 | background, watching and recompiling files as you change them. Compilation
25 | errors will be shown in the VSCode's "PROBLEMS" window.
26 |
27 | 2. Execute "Run Rest Task" from the Command Palette (Cmd+Shift+P) to re-run the
28 | test suite and lint the code for both programming and style errors. Linting
29 | errors will be shown in VSCode's "PROBLEMS" window. Failed tests are printed
30 | to terminal output only.
31 |
32 | ### Other editors/IDEs
33 |
34 | 1. Open a new terminal window/tab and start the continous build process via
35 | `npm run build:watch`. It will run TypeScript compiler in watch mode,
36 | recompiling files as you change them. Any compilation errors will be printed
37 | to the terminal.
38 |
39 | 2. In your main terminal window/tab, run `npm run test:dev` to re-run the test
40 | suite and lint the code for both programming and style errors. You should run
41 | this command manually whenever you have new changes to test. Test failures
42 | and linter errors will be printed to the terminal.
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2023] [SourceFuse]
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 |
2 |
3 | # [loopback4-authorization](https://github.com/sourcefuse/loopback4-authorization)
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ## Overview
30 |
31 | A LoopBack 4 extension for Authorization Capabilities. It's very simple to integration yet powerful and effective.
32 |
33 | ## Install
34 |
35 | ```sh
36 | npm install loopback4-authorization
37 | ```
38 |
39 | ## Quick Starter
40 |
41 | For a quick starter guide, you can refer to our [loopback 4 starter](https://github.com/sourcefuse/loopback4-starter) application which utilizes method #3 from the above in a simple multi-tenant application.
42 |
43 | ## Usage
44 |
45 | ### Ways of Integration:
46 |
47 | On a higher level, it provides three ways of integration:
48 |
49 | #### 1. User Level Permissions Only
50 |
51 | Where permissions are associated directly to user. In this case, each user entry in DB contains specific array of permission keys.
52 |
53 | #### 2. Role Based Permissions
54 |
55 | Where permissions are associated to roles and users have a specific role attached. This actually reduces redundancy in DB a lot, as most of the time, users will have many common permissions. If that is not the case for you, then, use the first method.
56 |
57 | #### 3. Role Based Permissions with User Level Flexibility
58 |
59 | This is the most flexible architecture. In this case, method #2 is implemented as is.
60 |
61 | On top of it, we also add user-level permissions override, allow/deny permissions over role permissions. So, say there is user who can perform all admin role actions except he cannot remove users from the system. So, DeleteUser permission can be denied at user level and role can be set as Admin for the user.
62 |
63 | [Extension enhancement using CASBIN authorisation](#Extension-enhancement-using-CASBIN-authorisation)
64 |
65 | Refer to the usage section below for details on integration.
66 |
67 | In order to use this component into your LoopBack application, please follow below steps.
68 |
69 | ### Steps
70 |
71 | #### Bind Component
72 |
73 | Add `AuthorizationComponent` to your application, Like below:
74 |
75 | ```ts
76 | this.bind(AuthorizationBindings.CONFIG).to({
77 | allowAlwaysPaths: ['/explorer'],
78 | });
79 | this.component(AuthorizationComponent);
80 | ```
81 |
82 | #### Implement Permission Interface
83 |
84 | If using method #1 from above, implement Permissions interface in User model and add permissions array.
85 |
86 | ```ts
87 | @model({
88 | name: 'users',
89 | })
90 | export class User extends Entity implements Permissions {
91 | // .....
92 | // other attributes here
93 | // .....
94 |
95 | @property({
96 | type: 'array',
97 | itemType: 'string',
98 | })
99 | permissions: string[];
100 |
101 | constructor(data?: Partial) {
102 | super(data);
103 | }
104 | }
105 | ```
106 |
107 | If using method #2 or #3 from above, implement Permissions interface in Role model and add permissions array.
108 |
109 | ```ts
110 | @model({
111 | name: 'roles',
112 | })
113 | export class Role extends Entity implements Permissions {
114 | // .....
115 | // other attributes here
116 | // .....
117 |
118 | @property({
119 | type: 'array',
120 | itemType: 'string',
121 | })
122 | permissions: string[];
123 |
124 | constructor(data?: Partial) {
125 | super(data);
126 | }
127 | }
128 | ```
129 |
130 | #### Implement `UserPermissionsOverride` Interface
131 |
132 | If using method #3 from above, implement UserPermissionsOverride interface in User model and add user level permissions array as below.
133 | Do this if there is a use-case of explicit allow/deny of permissions at user-level in the application.
134 | You can skip otherwise.
135 |
136 | ```ts
137 | @model({
138 | name: 'users',
139 | })
140 | export class User extends Entity implements UserPermissionsOverride {
141 | // .....
142 | // other attributes here
143 | // .....
144 |
145 | @property({
146 | type: 'array',
147 | itemType: 'object',
148 | })
149 | permissions: UserPermission[];
150 |
151 | constructor(data?: Partial) {
152 | super(data);
153 | }
154 | }
155 | ```
156 |
157 | #### User Permissions Provider
158 |
159 | For method #3, This extension exposes a provider function [AuthorizationBindings.USER_PERMISSIONS](https://github.com/sourcefuse/loopback4-authorization/blob/master/src/providers/user-permissions.provider.ts) to evaluate the user permissions based on its role permissions and user-level overrides.
160 |
161 | Just inject it like below:
162 |
163 | ```ts
164 | @inject(AuthorizationBindings.USER_PERMISSIONS)
165 | private readonly getUserPermissions: UserPermissionsFn,
166 | ```
167 |
168 | and invoke it
169 |
170 | ```ts
171 | const permissions = this.getUserPermissions(user.permissions, role.permissions);
172 | ```
173 |
174 | Add a step in custom sequence to check for authorization whenever any endpoint is hit.
175 |
176 | ```ts
177 | import {inject} from '@loopback/context';
178 | import {
179 | FindRoute,
180 | HttpErrors,
181 | InvokeMethod,
182 | ParseParams,
183 | Reject,
184 | RequestContext,
185 | RestBindings,
186 | Send,
187 | SequenceHandler,
188 | } from '@loopback/rest';
189 | import {AuthenticateFn, AuthenticationBindings} from 'loopback4-authentication';
190 | import {
191 | AuthorizationBindings,
192 | AuthorizeErrorKeys,
193 | AuthorizeFn,
194 | UserPermissionsFn,
195 | } from 'loopback4-authorization';
196 |
197 | import {AuthClient} from './models/auth-client.model';
198 | import {User} from './models/user.model';
199 |
200 | const SequenceActions = RestBindings.SequenceActions;
201 |
202 | export class MySequence implements SequenceHandler {
203 | constructor(
204 | @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
205 | @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
206 | @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
207 | @inject(SequenceActions.SEND) public send: Send,
208 | @inject(SequenceActions.REJECT) public reject: Reject,
209 | @inject(AuthenticationBindings.USER_AUTH_ACTION)
210 | protected authenticateRequest: AuthenticateFn,
211 | @inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
212 | protected authenticateRequestClient: AuthenticateFn,
213 | @inject(AuthorizationBindings.AUTHORIZE_ACTION)
214 | protected checkAuthorisation: AuthorizeFn,
215 | @inject(AuthorizationBindings.USER_PERMISSIONS)
216 | private readonly getUserPermissions: UserPermissionsFn,
217 | ) {}
218 |
219 | async handle(context: RequestContext) {
220 | const requestTime = Date.now();
221 | try {
222 | const {request, response} = context;
223 | const route = this.findRoute(request);
224 | const args = await this.parseParams(request, route);
225 | request.body = args[args.length - 1];
226 | await this.authenticateRequestClient(request);
227 | const authUser: User = await this.authenticateRequest(request);
228 |
229 | // Do ths if you are using method #3
230 | const permissions = this.getUserPermissions(
231 | authUser.permissions,
232 | authUser.role.permissions,
233 | );
234 | // This is the important line added for authorization. Needed for all 3 methods
235 | const isAccessAllowed: boolean = await this.checkAuthorisation(
236 | permissions, // do authUser.permissions if using method #1
237 | request,
238 | );
239 | // Checking access to route here
240 | if (!isAccessAllowed) {
241 | throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
242 | }
243 |
244 | const result = await this.invoke(route, args);
245 | this.send(response, result);
246 | } catch (err) {
247 | this.reject(context, err);
248 | }
249 | }
250 | }
251 | ```
252 |
253 | The above sequence also contains user authentication using [loopback4-authentication](https://github.com/sourcefuse/loopback4-authentication) package. You can refer to the documentation for the same for more details.
254 |
255 | Now we can add access permission keys to the controller methods using authorize decorator as below:
256 |
257 | ```ts
258 | @authorize(['CreateRole'])
259 | @post(rolesPath, {
260 | responses: {
261 | [STATUS_CODE.OK]: {
262 | description: 'Role model instance',
263 | content: {
264 | [CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
265 | },
266 | },
267 | },
268 | })
269 | async create(@requestBody() role: Role): Promise {
270 | return await this.roleRepository.create(role);
271 | }
272 | ```
273 |
274 | This endpoint will only be accessible if logged in user has permission
275 | `CreateRole`.
276 |
277 | A good practice is to keep all permission strings in a separate enum file like this.
278 |
279 | ```ts
280 | export const enum PermissionKey {
281 | ViewOwnUser = 'ViewOwnUser',
282 | ViewAnyUser = 'ViewAnyUser',
283 | ViewTenantUser = 'ViewTenantUser',
284 | CreateAnyUser = 'CreateAnyUser',
285 | CreateTenantUser = 'CreateTenantUser',
286 | UpdateOwnUser = 'UpdateOwnUser',
287 | UpdateTenantUser = 'UpdateTenantUser',
288 | UpdateAnyUser = 'UpdateAnyUser',
289 | DeleteTenantUser = 'DeleteTenantUser',
290 | DeleteAnyUser = 'DeleteAnyUser',
291 |
292 | ViewTenant = 'ViewTenant',
293 | CreateTenant = 'CreateTenant',
294 | UpdateTenant = 'UpdateTenant',
295 | DeleteTenant = 'DeleteTenant',
296 |
297 | ViewRole = 'ViewRole',
298 | CreateRole = 'CreateRole',
299 | UpdateRole = 'UpdateRole',
300 | DeleteRole = 'DeleteRole',
301 |
302 | ViewAudit = 'ViewAudit',
303 | CreateAudit = 'CreateAudit',
304 | UpdateAudit = 'UpdateAudit',
305 | DeleteAudit = 'DeleteAudit',
306 | }
307 | ```
308 |
309 | ### Overriding Permissions
310 |
311 | API endpoints provided by ARC API (aka Sourceloop) services have their permissions pre-defined in them bundled.
312 |
313 | In order to override them you can bind your custom permissions in the `AuthorizationBindings.PERMISSION` binding key.
314 | This accepts an object that should have Controller class name as the root level key and the value of which is another object of method to permissions array mapping.
315 |
316 | Like below:
317 |
318 | ```ts
319 | this.bind(AuthorizationBindings.PERMISSION).to({
320 | MessageController: {
321 | create: ['CreateMessage', 'ViewMessage'],
322 | updateAll: ['UpdateMessage', 'ViewMessage', 'ViewMessageNum']
323 | }
324 | AttachmentFileController: {
325 | create: ['CreateAttachmentFile', 'ViewAttachmentFile'],
326 | updateAll: ['UpdateAttachmentFile', 'ViewAttachmentFileNum']
327 | }
328 | });
329 | ```
330 |
331 | You can easily check the name of the controller and it's method name from the source code of the services or from the Swagger UI (clicking the endpoint in swagger append the controller and method name in the URL like `LoginController.login` where `login` is the method name).
332 |
333 | ## Serving the static files:
334 |
335 | Authorization configuration binding sets up paths that can be accessed without any authorization checks, allowing static files to be served directly from the root URL of the application.The allowAlwaysPaths property is used to define these paths for the files in public directory i.e for a test.html file in public directory ,one can provide its path as follows:
336 |
337 | ```
338 | this.bind(AuthorizationBindings.CONFIG).to({
339 | allowAlwaysPaths: ['/explorer','/test.html'],
340 | });
341 | ```
342 |
343 | To set up the public directory as a static,one can add the following in application.ts file.
344 |
345 | ```
346 | this.static('/', path.join(__dirname, '../public'));
347 |
348 | ```
349 |
350 | If, in case the file is in some other folder then `app.static()` can be called multiple times to configure the app to serve static assets from different directories.
351 |
352 | ```
353 | this.static('/', path.join(__dirname, '../public'));
354 | this.static('/downloads', path.join(__dirname, '../downloads'));
355 |
356 | ```
357 |
358 | For more details,refer [here](https://loopback.io/doc/en/lb4/Serving-static-files.html#:~:text=One%20of%20the%20basic%20requirements,the%20API%20are%20explained%20below.)
359 |
360 | ## Extension enhancement using CASBIN authorisation
361 |
362 | As a further enhancement to these methods, we are using [casbin library](https://casbin.org/docs/en/overview) to define permissions at level of entity or resource associated with an API call. Casbin authorisation implementation can be performed in two ways:
363 |
364 | 1. **Using default casbin policy document** - Define policy document in default casbin format in the app, and configure authorise decorator to use those policies.
365 | 2. **Defining custom logic to form dynamic policies** - Implement dynamic permissions based on app logic in casbin-enforcer-config provider. Authorisation extension will dynamically create casbin policy using this business logic to give the authorisation decisions.
366 |
367 | ### Casbin Usage
368 |
369 | In order to use this enhacement into your LoopBack application, please follow below steps.
370 |
371 | - Add providers to implement casbin authorisation along with authorisation component.
372 |
373 | ```ts
374 | this.bind(AuthorizationBindings.CONFIG).to({
375 | allowAlwaysPaths: ['/explorer'],
376 | });
377 | this.component(AuthorizationComponent);
378 |
379 | this.bind(AuthorizationBindings.CASBIN_ENFORCER_CONFIG_GETTER).toProvider(
380 | CasbinEnforcerConfigProvider,
381 | );
382 |
383 | this.bind(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN).toProvider(
384 | CasbinResValModifierProvider,
385 | );
386 | ```
387 |
388 | - Implement the **Casbin Resource value modifier provider**. Customise the resource value based on business logic using route arguments parameter in the provider.
389 |
390 | ```ts
391 | import {Getter, inject, Provider} from '@loopback/context';
392 | import {HttpErrors} from '@loopback/rest';
393 | import {
394 | AuthorizationBindings,
395 | AuthorizationMetadata,
396 | CasbinResourceModifierFn,
397 | } from 'loopback4-authorization';
398 |
399 | export class CasbinResValModifierProvider
400 | implements Provider
401 | {
402 | constructor(
403 | @inject.getter(AuthorizationBindings.METADATA)
404 | private readonly getCasbinMetadata: Getter,
405 | @inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)
406 | private readonly allowAlwaysPath: string[],
407 | ) {}
408 |
409 | value(): CasbinResourceModifierFn {
410 | return (pathParams: string[], req: Request) => this.action(pathParams, req);
411 | }
412 |
413 | async action(pathParams: string[], req: Request): Promise {
414 | const metadata: AuthorizationMetadata = await this.getCasbinMetadata();
415 |
416 | if (
417 | !metadata &&
418 | !!this.allowAlwaysPath.find(path => req.path.indexOf(path) === 0)
419 | ) {
420 | return '';
421 | }
422 |
423 | if (!metadata) {
424 | throw new HttpErrors.InternalServerError(`Metadata object not found`);
425 | }
426 | const res = metadata.resource;
427 |
428 | // Now modify the resource parameter using on path params, as per business logic.
429 | // Returning resource value as such for default case.
430 |
431 | return `${res}`;
432 | }
433 | }
434 | ```
435 |
436 | - Implement the **casbin enforcer config provider** . Provide the casbin model path. Model definition can be initialized from [.CONF file, from code, or from a string](https://casbin.org/docs/en/model-storage).
437 | In the case of policy creation being handled by extension (isCasbinPolicy parameter is false), provide the array of Resource-Permission objects for a given user, based on business logic.
438 | In other case, provide the policy from file or as CSV string or from [casbin Adapters](https://casbin.org/docs/en/adapters).
439 | **NOTE**: In the second case, if model is initialized from .CONF file, then any of the above formats can be used for policy. But if model is being initialised from code or string, then policy should be provided as [casbin adapter](https://casbin.org/docs/en/adapters) only.
440 |
441 | ```ts
442 | import {Provider} from '@loopback/context';
443 | import {
444 | CasbinConfig,
445 | CasbinEnforcerConfigGetterFn,
446 | IAuthUserWithPermissions,
447 | } from 'loopback4-authorization';
448 | import * as path from 'path';
449 |
450 | export class CasbinEnforcerConfigProvider
451 | implements Provider
452 | {
453 | constructor() {}
454 |
455 | value(): CasbinEnforcerConfigGetterFn {
456 | return (
457 | authUser: IAuthUserWithPermissions,
458 | resource: string,
459 | isCasbinPolicy?: boolean,
460 | ) => this.action(authUser, resource, isCasbinPolicy);
461 | }
462 |
463 | async action(
464 | authUser: IAuthUserWithPermissions,
465 | resource: string,
466 | isCasbinPolicy?: boolean,
467 | ): Promise {
468 | const model = path.resolve(__dirname, './../../fixtures/casbin/model.conf'); // Model initialization from file path
469 | /**
470 | * import * as casbin from 'casbin';
471 | *
472 | * To initialize model from code, use
473 | * let m = new casbin.Model();
474 | * m.addDef('r', 'r', 'sub, obj, act'); and so on...
475 | *
476 | * To initialize model from string, use
477 | * const text = `
478 | * [request_definition]
479 | * r = sub, obj, act
480 | *
481 | * [policy_definition]
482 | * p = sub, obj, act
483 | *
484 | * [policy_effect]
485 | * e = some(where (p.eft == allow))
486 | *
487 | * [matchers]
488 | * m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
489 | * `;
490 | * const model = casbin.newModelFromString(text);
491 | */
492 |
493 | // Write business logic to find out the allowed resource-permission sets for this user. Below is a dummy value.
494 | //const allowedRes = [{resource: 'session', permission: "CreateMeetingSession"}];
495 |
496 | const policy = path.resolve(
497 | __dirname,
498 | './../../fixtures/casbin/policy.csv',
499 | );
500 |
501 | const result: CasbinConfig = {
502 | model,
503 | //allowedRes,
504 | policy,
505 | };
506 | return result;
507 | }
508 | }
509 | ```
510 |
511 | - Add the dependency injections for resource value modifer provider, and casbin authorisation function in the sequence.ts
512 |
513 | ```ts
514 | @inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
515 | protected checkAuthorisation: CasbinAuthorizeFn,
516 | @inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
517 | protected casbinResModifierFn: CasbinResourceModifierFn,
518 | ```
519 |
520 | - Add a step in custom sequence to check for authorization whenever any end
521 | point is hit.
522 |
523 | ```ts
524 | import {inject} from '@loopback/context';
525 | import {
526 | FindRoute,
527 | HttpErrors,
528 | InvokeMethod,
529 | ParseParams,
530 | Reject,
531 | RequestContext,
532 | RestBindings,
533 | Send,
534 | SequenceHandler,
535 | } from '@loopback/rest';
536 | import {AuthenticateFn, AuthenticationBindings} from 'loopback4-authentication';
537 | import {
538 | AuthorizationBindings,
539 | AuthorizeErrorKeys,
540 | AuthorizeFn,
541 | UserPermissionsFn,
542 | } from 'loopback4-authorization';
543 |
544 | import {AuthClient} from './models/auth-client.model';
545 | import {User} from './models/user.model';
546 |
547 | const SequenceActions = RestBindings.SequenceActions;
548 |
549 | export class MySequence implements SequenceHandler {
550 | constructor(
551 | @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
552 | @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
553 | @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
554 | @inject(SequenceActions.SEND) public send: Send,
555 | @inject(SequenceActions.REJECT) public reject: Reject,
556 | @inject(AuthenticationBindings.USER_AUTH_ACTION)
557 | protected authenticateRequest: AuthenticateFn,
558 | @inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
559 | protected authenticateRequestClient: AuthenticateFn,
560 | @inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
561 | protected checkAuthorisation: CasbinAuthorizeFn,
562 | @inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
563 | protected casbinResModifierFn: CasbinResourceModifierFn,
564 | ) {}
565 |
566 | async handle(context: RequestContext) {
567 | const requestTime = Date.now();
568 | try {
569 | const {request, response} = context;
570 | const route = this.findRoute(request);
571 | const args = await this.parseParams(request, route);
572 | request.body = args[args.length - 1];
573 | await this.authenticateRequestClient(request);
574 | const authUser: User = await this.authenticateRequest(request);
575 |
576 | // Invoke Resource value modifier
577 | const resVal = await this.casbinResModifierFn(args);
578 |
579 | // Check authorisation
580 | const isAccessAllowed: boolean = await this.checkAuthorisation(
581 | authUser,
582 | resVal,
583 | request,
584 | );
585 | // Checking access to route here
586 | if (!isAccessAllowed) {
587 | throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
588 | }
589 |
590 | const result = await this.invoke(route, args);
591 | this.send(response, result);
592 | } catch (err) {
593 | this.reject(context, err);
594 | }
595 | }
596 | }
597 | ```
598 |
599 | - Now we can add access permission keys to the controller methods using authorize
600 | decorator as below. Set isCasbinPolicy parameter to use casbin default policy format. Default is false.
601 |
602 | ```ts
603 | @authorize({permissions: ['CreateRole'], resource:'role', isCasbinPolicy: true})
604 | @post(rolesPath, {
605 | responses: {
606 | [STATUS_CODE.OK]: {
607 | description: 'Role model instance',
608 | content: {
609 | [CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
610 | },
611 | },
612 | },
613 | })
614 | async create(@requestBody() role: Role): Promise {
615 | return await this.roleRepository.create(role);
616 | }
617 | ```
618 |
619 | ## Feedback
620 |
621 | If you've noticed a bug or have a question or have a feature request, [search the issue tracker](https://github.com/sourcefuse/loopback4-authorization/issues) to see if someone else in the community has already created a ticket.
622 | If not, go ahead and [make one](https://github.com/sourcefuse/loopback4-authorization/issues/new/choose)!
623 | All feature requests are welcome. Implementation time may vary. Feel free to contribute the same, if you can.
624 | If you think this extension is useful, please [star](https://help.github.com/en/articles/about-stars) it. Appreciation really helps in keeping this project alive.
625 |
626 | ## Contributing
627 |
628 | Please read [CONTRIBUTING.md](https://github.com/sourcefuse/loopback4-authorization/blob/master/.github/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
629 |
630 | ## Code of conduct
631 |
632 | Code of conduct guidelines [here](https://github.com/sourcefuse/loopback4-authorization/blob/master/.github/CODE_OF_CONDUCT.md).
633 |
634 | ## License
635 |
636 | [MIT](https://github.com/sourcefuse/loopback4-authorization/blob/master/LICENSE)
637 |
--------------------------------------------------------------------------------
/catalog-info.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: backstage.io/v1alpha1
2 | kind: Component
3 | metadata:
4 | name: loopback4-authorization
5 | annotations:
6 | github.com/project-slug: sourcefuse/loopback4-authorization
7 | backstage.io/techdocs-ref: dir:.
8 | namespace: arc
9 | description: A LoopBack 4 extension for managing API Authorization.
10 | tags:
11 | - authorization
12 | - access-management
13 | - loopback
14 | - extension
15 | links:
16 | - url: https://npmjs.com/package/loopback4-authorization
17 | title: NPM Package
18 | - url: https://loopback.io/doc/en/lb4/Extending-LoopBack-4.html#overview
19 | title: Extending LoopBack
20 | spec:
21 | type: component
22 | lifecycle: production
23 | owner: sourcefuse
24 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | 'header-max-length': [2, 'always', 100],
5 | 'body-leading-blank': [2, 'always'],
6 | 'footer-leading-blank': [0, 'always'],
7 | 'references-empty': [2, 'never'],
8 | 'body-empty': [2, 'never'],
9 | },
10 | parserPreset: {
11 | parserOpts: {
12 | issuePrefixes: ['GH-'],
13 | },
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # [loopback4-authorization](https://github.com/sourcefuse/loopback4-authorization)
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ## Overview
30 |
31 | A LoopBack 4 extension for Authorization Capabilities. It's very simple to integration yet powerful and effective.
32 |
33 | ## Install
34 |
35 | ```sh
36 | npm install loopback4-authorization
37 | ```
38 |
39 | ## Quick Starter
40 |
41 | For a quick starter guide, you can refer to our [loopback 4 starter](https://github.com/sourcefuse/loopback4-starter) application which utilizes method #3 from the above in a simple multi-tenant application.
42 |
43 | ## Usage
44 |
45 | ### Ways of Integration:
46 |
47 | On a higher level, it provides three ways of integration:
48 |
49 | #### 1. User Level Permissions Only
50 |
51 | Where permissions are associated directly to user. In this case, each user entry in DB contains specific array of permission keys.
52 |
53 | #### 2. Role Based Permissions
54 |
55 | Where permissions are associated to roles and users have a specific role attached. This actually reduces redundancy in DB a lot, as most of the time, users will have many common permissions. If that is not the case for you, then, use the first method.
56 |
57 | #### 3. Role Based Permissions with User Level Flexibility
58 |
59 | This is the most flexible architecture. In this case, method #2 is implemented as is.
60 |
61 | On top of it, we also add user-level permissions override, allow/deny permissions over role permissions. So, say there is user who can perform all admin role actions except he cannot remove users from the system. So, DeleteUser permission can be denied at user level and role can be set as Admin for the user.
62 |
63 | [Extension enhancement using CASBIN authorisation](#Extension-enhancement-using-CASBIN-authorisation)
64 |
65 | Refer to the usage section below for details on integration.
66 |
67 | In order to use this component into your LoopBack application, please follow below steps.
68 |
69 | ### Steps
70 |
71 | #### Bind Component
72 |
73 | Add `AuthorizationComponent` to your application, Like below:
74 |
75 | ```ts
76 | this.bind(AuthorizationBindings.CONFIG).to({
77 | allowAlwaysPaths: ['/explorer'],
78 | });
79 | this.component(AuthorizationComponent);
80 | ```
81 |
82 | #### Implement Permission Interface
83 |
84 | If using method #1 from above, implement Permissions interface in User model and add permissions array.
85 |
86 | ```ts
87 | @model({
88 | name: 'users',
89 | })
90 | export class User extends Entity implements Permissions {
91 | // .....
92 | // other attributes here
93 | // .....
94 |
95 | @property({
96 | type: 'array',
97 | itemType: 'string',
98 | })
99 | permissions: string[];
100 |
101 | constructor(data?: Partial) {
102 | super(data);
103 | }
104 | }
105 | ```
106 |
107 | If using method #2 or #3 from above, implement Permissions interface in Role model and add permissions array.
108 |
109 | ```ts
110 | @model({
111 | name: 'roles',
112 | })
113 | export class Role extends Entity implements Permissions {
114 | // .....
115 | // other attributes here
116 | // .....
117 |
118 | @property({
119 | type: 'array',
120 | itemType: 'string',
121 | })
122 | permissions: string[];
123 |
124 | constructor(data?: Partial) {
125 | super(data);
126 | }
127 | }
128 | ```
129 |
130 | #### Implement `UserPermissionsOverride` Interface
131 |
132 | If using method #3 from above, implement UserPermissionsOverride interface in User model and add user level permissions array as below.
133 | Do this if there is a use-case of explicit allow/deny of permissions at user-level in the application.
134 | You can skip otherwise.
135 |
136 | ```ts
137 | @model({
138 | name: 'users',
139 | })
140 | export class User extends Entity implements UserPermissionsOverride {
141 | // .....
142 | // other attributes here
143 | // .....
144 |
145 | @property({
146 | type: 'array',
147 | itemType: 'object',
148 | })
149 | permissions: UserPermission[];
150 |
151 | constructor(data?: Partial) {
152 | super(data);
153 | }
154 | }
155 | ```
156 |
157 | #### User Permissions Provider
158 |
159 | For method #3, This extension exposes a provider function [AuthorizationBindings.USER_PERMISSIONS](https://github.com/sourcefuse/loopback4-authorization/blob/master/src/providers/user-permissions.provider.ts) to evaluate the user permissions based on its role permissions and user-level overrides.
160 |
161 | Just inject it like below:
162 |
163 | ```ts
164 | @inject(AuthorizationBindings.USER_PERMISSIONS)
165 | private readonly getUserPermissions: UserPermissionsFn,
166 | ```
167 |
168 | and invoke it
169 |
170 | ```ts
171 | const permissions = this.getUserPermissions(user.permissions, role.permissions);
172 | ```
173 |
174 | Add a step in custom sequence to check for authorization whenever any endpoint is hit.
175 |
176 | ```ts
177 | import {inject} from '@loopback/context';
178 | import {
179 | FindRoute,
180 | HttpErrors,
181 | InvokeMethod,
182 | ParseParams,
183 | Reject,
184 | RequestContext,
185 | RestBindings,
186 | Send,
187 | SequenceHandler,
188 | } from '@loopback/rest';
189 | import {AuthenticateFn, AuthenticationBindings} from 'loopback4-authentication';
190 | import {
191 | AuthorizationBindings,
192 | AuthorizeErrorKeys,
193 | AuthorizeFn,
194 | UserPermissionsFn,
195 | } from 'loopback4-authorization';
196 |
197 | import {AuthClient} from './models/auth-client.model';
198 | import {User} from './models/user.model';
199 |
200 | const SequenceActions = RestBindings.SequenceActions;
201 |
202 | export class MySequence implements SequenceHandler {
203 | constructor(
204 | @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
205 | @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
206 | @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
207 | @inject(SequenceActions.SEND) public send: Send,
208 | @inject(SequenceActions.REJECT) public reject: Reject,
209 | @inject(AuthenticationBindings.USER_AUTH_ACTION)
210 | protected authenticateRequest: AuthenticateFn,
211 | @inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
212 | protected authenticateRequestClient: AuthenticateFn,
213 | @inject(AuthorizationBindings.AUTHORIZE_ACTION)
214 | protected checkAuthorisation: AuthorizeFn,
215 | @inject(AuthorizationBindings.USER_PERMISSIONS)
216 | private readonly getUserPermissions: UserPermissionsFn,
217 | ) {}
218 |
219 | async handle(context: RequestContext) {
220 | const requestTime = Date.now();
221 | try {
222 | const {request, response} = context;
223 | const route = this.findRoute(request);
224 | const args = await this.parseParams(request, route);
225 | request.body = args[args.length - 1];
226 | await this.authenticateRequestClient(request);
227 | const authUser: User = await this.authenticateRequest(request);
228 |
229 | // Do ths if you are using method #3
230 | const permissions = this.getUserPermissions(
231 | authUser.permissions,
232 | authUser.role.permissions,
233 | );
234 | // This is the important line added for authorization. Needed for all 3 methods
235 | const isAccessAllowed: boolean = await this.checkAuthorisation(
236 | permissions, // do authUser.permissions if using method #1
237 | request,
238 | );
239 | // Checking access to route here
240 | if (!isAccessAllowed) {
241 | throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
242 | }
243 |
244 | const result = await this.invoke(route, args);
245 | this.send(response, result);
246 | } catch (err) {
247 | this.reject(context, err);
248 | }
249 | }
250 | }
251 | ```
252 |
253 | The above sequence also contains user authentication using [loopback4-authentication](https://github.com/sourcefuse/loopback4-authentication) package. You can refer to the documentation for the same for more details.
254 |
255 | Now we can add access permission keys to the controller methods using authorize decorator as below:
256 |
257 | ```ts
258 | @authorize(['CreateRole'])
259 | @post(rolesPath, {
260 | responses: {
261 | [STATUS_CODE.OK]: {
262 | description: 'Role model instance',
263 | content: {
264 | [CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
265 | },
266 | },
267 | },
268 | })
269 | async create(@requestBody() role: Role): Promise {
270 | return await this.roleRepository.create(role);
271 | }
272 | ```
273 |
274 | This endpoint will only be accessible if logged in user has permission
275 | `CreateRole`.
276 |
277 | A good practice is to keep all permission strings in a separate enum file like this.
278 |
279 | ```ts
280 | export const enum PermissionKey {
281 | ViewOwnUser = 'ViewOwnUser',
282 | ViewAnyUser = 'ViewAnyUser',
283 | ViewTenantUser = 'ViewTenantUser',
284 | CreateAnyUser = 'CreateAnyUser',
285 | CreateTenantUser = 'CreateTenantUser',
286 | UpdateOwnUser = 'UpdateOwnUser',
287 | UpdateTenantUser = 'UpdateTenantUser',
288 | UpdateAnyUser = 'UpdateAnyUser',
289 | DeleteTenantUser = 'DeleteTenantUser',
290 | DeleteAnyUser = 'DeleteAnyUser',
291 |
292 | ViewTenant = 'ViewTenant',
293 | CreateTenant = 'CreateTenant',
294 | UpdateTenant = 'UpdateTenant',
295 | DeleteTenant = 'DeleteTenant',
296 |
297 | ViewRole = 'ViewRole',
298 | CreateRole = 'CreateRole',
299 | UpdateRole = 'UpdateRole',
300 | DeleteRole = 'DeleteRole',
301 |
302 | ViewAudit = 'ViewAudit',
303 | CreateAudit = 'CreateAudit',
304 | UpdateAudit = 'UpdateAudit',
305 | DeleteAudit = 'DeleteAudit',
306 | }
307 | ```
308 |
309 | ### Overriding Permissions
310 |
311 | API endpoints provided by ARC API (aka Sourceloop) services have their permissions pre-defined in them bundled.
312 |
313 | In order to override them you can bind your custom permissions in the `AuthorizationBindings.PERMISSION` binding key.
314 | This accepts an object that should have Controller class name as the root level key and the value of which is another object of method to permissions array mapping.
315 |
316 | Like below:
317 |
318 | ```ts
319 | this.bind(AuthorizationBindings.PERMISSION).to({
320 | MessageController: {
321 | create: ['CreateMessage', 'ViewMessage'],
322 | updateAll: ['UpdateMessage', 'ViewMessage', 'ViewMessageNum']
323 | }
324 | AttachmentFileController: {
325 | create: ['CreateAttachmentFile', 'ViewAttachmentFile'],
326 | updateAll: ['UpdateAttachmentFile', 'ViewAttachmentFileNum']
327 | }
328 | });
329 | ```
330 |
331 | You can easily check the name of the controller and it's method name from the source code of the services or from the Swagger UI (clicking the endpoint in swagger append the controller and method name in the URL like `LoginController.login` where `login` is the method name).
332 |
333 | ## Serving the static files:
334 |
335 | Authorization configuration binding sets up paths that can be accessed without any authorization checks, allowing static files to be served directly from the root URL of the application.The allowAlwaysPaths property is used to define these paths for the files in public directory i.e for a test.html file in public directory ,one can provide its path as follows:
336 |
337 | ```
338 | this.bind(AuthorizationBindings.CONFIG).to({
339 | allowAlwaysPaths: ['/explorer','/test.html'],
340 | });
341 | ```
342 |
343 | To set up the public directory as a static,one can add the following in application.ts file.
344 |
345 | ```
346 | this.static('/', path.join(__dirname, '../public'));
347 |
348 | ```
349 |
350 | If, in case the file is in some other folder then `app.static()` can be called multiple times to configure the app to serve static assets from different directories.
351 |
352 | ```
353 | this.static('/', path.join(__dirname, '../public'));
354 | this.static('/downloads', path.join(__dirname, '../downloads'));
355 |
356 | ```
357 |
358 | For more details,refer [here](https://loopback.io/doc/en/lb4/Serving-static-files.html#:~:text=One%20of%20the%20basic%20requirements,the%20API%20are%20explained%20below.)
359 |
360 | ## Extension enhancement using CASBIN authorisation
361 |
362 | As a further enhancement to these methods, we are using [casbin library](https://casbin.org/docs/en/overview) to define permissions at level of entity or resource associated with an API call. Casbin authorisation implementation can be performed in two ways:
363 |
364 | 1. **Using default casbin policy document** - Define policy document in default casbin format in the app, and configure authorise decorator to use those policies.
365 | 2. **Defining custom logic to form dynamic policies** - Implement dynamic permissions based on app logic in casbin-enforcer-config provider. Authorisation extension will dynamically create casbin policy using this business logic to give the authorisation decisions.
366 |
367 | ### Casbin Usage
368 |
369 | In order to use this enhacement into your LoopBack application, please follow below steps.
370 |
371 | - Add providers to implement casbin authorisation along with authorisation component.
372 |
373 | ```ts
374 | this.bind(AuthorizationBindings.CONFIG).to({
375 | allowAlwaysPaths: ['/explorer'],
376 | });
377 | this.component(AuthorizationComponent);
378 |
379 | this.bind(AuthorizationBindings.CASBIN_ENFORCER_CONFIG_GETTER).toProvider(
380 | CasbinEnforcerConfigProvider,
381 | );
382 |
383 | this.bind(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN).toProvider(
384 | CasbinResValModifierProvider,
385 | );
386 | ```
387 |
388 | - Implement the **Casbin Resource value modifier provider**. Customise the resource value based on business logic using route arguments parameter in the provider.
389 |
390 | ```ts
391 | import {Getter, inject, Provider} from '@loopback/context';
392 | import {HttpErrors} from '@loopback/rest';
393 | import {
394 | AuthorizationBindings,
395 | AuthorizationMetadata,
396 | CasbinResourceModifierFn,
397 | } from 'loopback4-authorization';
398 |
399 | export class CasbinResValModifierProvider
400 | implements Provider
401 | {
402 | constructor(
403 | @inject.getter(AuthorizationBindings.METADATA)
404 | private readonly getCasbinMetadata: Getter,
405 | @inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)
406 | private readonly allowAlwaysPath: string[],
407 | ) {}
408 |
409 | value(): CasbinResourceModifierFn {
410 | return (pathParams: string[], req: Request) => this.action(pathParams, req);
411 | }
412 |
413 | async action(pathParams: string[], req: Request): Promise {
414 | const metadata: AuthorizationMetadata = await this.getCasbinMetadata();
415 |
416 | if (
417 | !metadata &&
418 | !!this.allowAlwaysPath.find(path => req.path.indexOf(path) === 0)
419 | ) {
420 | return '';
421 | }
422 |
423 | if (!metadata) {
424 | throw new HttpErrors.InternalServerError(`Metadata object not found`);
425 | }
426 | const res = metadata.resource;
427 |
428 | // Now modify the resource parameter using on path params, as per business logic.
429 | // Returning resource value as such for default case.
430 |
431 | return `${res}`;
432 | }
433 | }
434 | ```
435 |
436 | - Implement the **casbin enforcer config provider** . Provide the casbin model path. Model definition can be initialized from [.CONF file, from code, or from a string](https://casbin.org/docs/en/model-storage).
437 | In the case of policy creation being handled by extension (isCasbinPolicy parameter is false), provide the array of Resource-Permission objects for a given user, based on business logic.
438 | In other case, provide the policy from file or as CSV string or from [casbin Adapters](https://casbin.org/docs/en/adapters).
439 | **NOTE**: In the second case, if model is initialized from .CONF file, then any of the above formats can be used for policy. But if model is being initialised from code or string, then policy should be provided as [casbin adapter](https://casbin.org/docs/en/adapters) only.
440 |
441 | ```ts
442 | import {Provider} from '@loopback/context';
443 | import {
444 | CasbinConfig,
445 | CasbinEnforcerConfigGetterFn,
446 | IAuthUserWithPermissions,
447 | } from 'loopback4-authorization';
448 | import * as path from 'path';
449 |
450 | export class CasbinEnforcerConfigProvider
451 | implements Provider
452 | {
453 | constructor() {}
454 |
455 | value(): CasbinEnforcerConfigGetterFn {
456 | return (
457 | authUser: IAuthUserWithPermissions,
458 | resource: string,
459 | isCasbinPolicy?: boolean,
460 | ) => this.action(authUser, resource, isCasbinPolicy);
461 | }
462 |
463 | async action(
464 | authUser: IAuthUserWithPermissions,
465 | resource: string,
466 | isCasbinPolicy?: boolean,
467 | ): Promise {
468 | const model = path.resolve(__dirname, './../../fixtures/casbin/model.conf'); // Model initialization from file path
469 | /**
470 | * import * as casbin from 'casbin';
471 | *
472 | * To initialize model from code, use
473 | * let m = new casbin.Model();
474 | * m.addDef('r', 'r', 'sub, obj, act'); and so on...
475 | *
476 | * To initialize model from string, use
477 | * const text = `
478 | * [request_definition]
479 | * r = sub, obj, act
480 | *
481 | * [policy_definition]
482 | * p = sub, obj, act
483 | *
484 | * [policy_effect]
485 | * e = some(where (p.eft == allow))
486 | *
487 | * [matchers]
488 | * m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
489 | * `;
490 | * const model = casbin.newModelFromString(text);
491 | */
492 |
493 | // Write business logic to find out the allowed resource-permission sets for this user. Below is a dummy value.
494 | //const allowedRes = [{resource: 'session', permission: "CreateMeetingSession"}];
495 |
496 | const policy = path.resolve(
497 | __dirname,
498 | './../../fixtures/casbin/policy.csv',
499 | );
500 |
501 | const result: CasbinConfig = {
502 | model,
503 | //allowedRes,
504 | policy,
505 | };
506 | return result;
507 | }
508 | }
509 | ```
510 |
511 | - Add the dependency injections for resource value modifer provider, and casbin authorisation function in the sequence.ts
512 |
513 | ```ts
514 | @inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
515 | protected checkAuthorisation: CasbinAuthorizeFn,
516 | @inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
517 | protected casbinResModifierFn: CasbinResourceModifierFn,
518 | ```
519 |
520 | - Add a step in custom sequence to check for authorization whenever any end
521 | point is hit.
522 |
523 | ```ts
524 | import {inject} from '@loopback/context';
525 | import {
526 | FindRoute,
527 | HttpErrors,
528 | InvokeMethod,
529 | ParseParams,
530 | Reject,
531 | RequestContext,
532 | RestBindings,
533 | Send,
534 | SequenceHandler,
535 | } from '@loopback/rest';
536 | import {AuthenticateFn, AuthenticationBindings} from 'loopback4-authentication';
537 | import {
538 | AuthorizationBindings,
539 | AuthorizeErrorKeys,
540 | AuthorizeFn,
541 | UserPermissionsFn,
542 | } from 'loopback4-authorization';
543 |
544 | import {AuthClient} from './models/auth-client.model';
545 | import {User} from './models/user.model';
546 |
547 | const SequenceActions = RestBindings.SequenceActions;
548 |
549 | export class MySequence implements SequenceHandler {
550 | constructor(
551 | @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
552 | @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
553 | @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
554 | @inject(SequenceActions.SEND) public send: Send,
555 | @inject(SequenceActions.REJECT) public reject: Reject,
556 | @inject(AuthenticationBindings.USER_AUTH_ACTION)
557 | protected authenticateRequest: AuthenticateFn,
558 | @inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
559 | protected authenticateRequestClient: AuthenticateFn,
560 | @inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
561 | protected checkAuthorisation: CasbinAuthorizeFn,
562 | @inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
563 | protected casbinResModifierFn: CasbinResourceModifierFn,
564 | ) {}
565 |
566 | async handle(context: RequestContext) {
567 | const requestTime = Date.now();
568 | try {
569 | const {request, response} = context;
570 | const route = this.findRoute(request);
571 | const args = await this.parseParams(request, route);
572 | request.body = args[args.length - 1];
573 | await this.authenticateRequestClient(request);
574 | const authUser: User = await this.authenticateRequest(request);
575 |
576 | // Invoke Resource value modifier
577 | const resVal = await this.casbinResModifierFn(args);
578 |
579 | // Check authorisation
580 | const isAccessAllowed: boolean = await this.checkAuthorisation(
581 | authUser,
582 | resVal,
583 | request,
584 | );
585 | // Checking access to route here
586 | if (!isAccessAllowed) {
587 | throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
588 | }
589 |
590 | const result = await this.invoke(route, args);
591 | this.send(response, result);
592 | } catch (err) {
593 | this.reject(context, err);
594 | }
595 | }
596 | }
597 | ```
598 |
599 | - Now we can add access permission keys to the controller methods using authorize
600 | decorator as below. Set isCasbinPolicy parameter to use casbin default policy format. Default is false.
601 |
602 | ```ts
603 | @authorize({permissions: ['CreateRole'], resource:'role', isCasbinPolicy: true})
604 | @post(rolesPath, {
605 | responses: {
606 | [STATUS_CODE.OK]: {
607 | description: 'Role model instance',
608 | content: {
609 | [CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
610 | },
611 | },
612 | },
613 | })
614 | async create(@requestBody() role: Role): Promise {
615 | return await this.roleRepository.create(role);
616 | }
617 | ```
618 |
619 | ## Feedback
620 |
621 | If you've noticed a bug or have a question or have a feature request, [search the issue tracker](https://github.com/sourcefuse/loopback4-authorization/issues) to see if someone else in the community has already created a ticket.
622 | If not, go ahead and [make one](https://github.com/sourcefuse/loopback4-authorization/issues/new/choose)!
623 | All feature requests are welcome. Implementation time may vary. Feel free to contribute the same, if you can.
624 | If you think this extension is useful, please [star](https://help.github.com/en/articles/about-stars) it. Appreciation really helps in keeping this project alive.
625 |
626 | ## Contributing
627 |
628 | Please read [CONTRIBUTING.md](https://github.com/sourcefuse/loopback4-authorization/blob/master/.github/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
629 |
630 | ## Code of conduct
631 |
632 | Code of conduct guidelines [here](https://github.com/sourcefuse/loopback4-authorization/blob/master/.github/CODE_OF_CONDUCT.md).
633 |
634 | ## License
635 |
636 | [MIT](https://github.com/sourcefuse/loopback4-authorization/blob/master/LICENSE)
637 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: loopback4-authorization
2 | site_description: loopback4-authorization
3 |
4 | plugins:
5 | - techdocs-core
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loopback4-authorization",
3 | "version": "7.0.3",
4 | "description": "ARC authorization extension for loopback-next applications.",
5 | "keywords": [
6 | "loopback-extension",
7 | "loopback",
8 | "loopback-next",
9 | "authorization",
10 | "authorisation"
11 | ],
12 | "main": "dist/index.js",
13 | "types": "dist/index.d.ts",
14 | "engines": {
15 | "node": ">=18"
16 | },
17 | "scripts": {
18 | "build": "npm run clean && lb-tsc",
19 | "build:watch": "lb-tsc --watch",
20 | "clean": "lb-clean dist *.tsbuildinfo .eslintcache",
21 | "lint": "npm run prettier:check && npm run eslint",
22 | "lint:fix": "npm run eslint:fix && npm run prettier:fix",
23 | "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"",
24 | "prettier:check": "npm run prettier:cli -- -l",
25 | "prettier:fix": "npm run prettier:cli -- --write",
26 | "eslint": "lb-eslint --report-unused-disable-directives .",
27 | "eslint:fix": "npm run eslint -- --fix",
28 | "pretest": "npm run build",
29 | "test": "lb-mocha --allow-console-logs \"dist/__tests__\"",
30 | "coverage": "lb-nyc npm run test",
31 | "coverage:ci": "lb-nyc report --reporter=text-lcov | coveralls",
32 | "posttest": "npm run lint",
33 | "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest",
34 | "prepublishOnly": "npm run build && npm run lint",
35 | "prepare": "husky install"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "https://github.com/sourcefuse/loopback4-authorization"
40 | },
41 | "author": "Sourcefuse",
42 | "license": "MIT",
43 | "files": [
44 | "README.md",
45 | "dist",
46 | "src",
47 | "!*/__tests__"
48 | ],
49 | "dependencies": {
50 | "@loopback/core": "^6.1.6",
51 | "casbin": "^5.32.0",
52 | "casbin-pg-adapter": "^1.4.0",
53 | "lodash": "^4.17.21"
54 | },
55 | "devDependencies": {
56 | "@commitlint/cli": "^17.7.1",
57 | "@commitlint/config-conventional": "^17.7.0",
58 | "@loopback/boot": "^7.0.9",
59 | "@loopback/build": "^11.0.8",
60 | "@loopback/context": "^7.0.9",
61 | "@loopback/eslint-config": "^15.0.4",
62 | "@loopback/rest": "^14.0.9",
63 | "@loopback/testlab": "^7.0.8",
64 | "@semantic-release/changelog": "^6.0.1",
65 | "@semantic-release/commit-analyzer": "^9.0.2",
66 | "@semantic-release/git": "^10.0.1",
67 | "@semantic-release/npm": "^9.0.1",
68 | "@semantic-release/release-notes-generator": "^10.0.3",
69 | "@types/lodash": "^4.14.181",
70 | "@types/node": "^18.11.9",
71 | "commitizen": "^4.2.4",
72 | "cz-conventional-changelog": "^3.3.0",
73 | "cz-customizable": "^6.3.0",
74 | "cz-customizable-ghooks": "^2.0.0",
75 | "eslint": "^8.57.0",
76 | "fs-extra": "^11.2.0",
77 | "git-release-notes": "^5.0.0",
78 | "husky": "^7.0.4",
79 | "jsdom": "^21.0.0",
80 | "semantic-release": "^19.0.3",
81 | "simple-git": "^3.15.1",
82 | "typescript": "~5.2.2"
83 | },
84 | "publishConfig": {
85 | "registry": "https://registry.npmjs.org/"
86 | },
87 | "overrides": {
88 | "git-release-notes": {
89 | "ejs": "^3.1.8",
90 | "yargs": "^17.6.2"
91 | },
92 | "@semantic-release/npm": {
93 | "npm": "^9.4.2"
94 | }
95 | },
96 | "config": {
97 | "commitizen": {
98 | "path": "./node_modules/cz-customizable"
99 | }
100 | },
101 | "release": {
102 | "branches": [
103 | "master"
104 | ],
105 | "plugins": [
106 | [
107 | "@semantic-release/commit-analyzer",
108 | {
109 | "preset": "angular",
110 | "releaseRules": [
111 | {
112 | "type": "chore",
113 | "scope": "deps",
114 | "release": "patch"
115 | }
116 | ]
117 | }
118 | ],
119 | "@semantic-release/release-notes-generator",
120 | "@semantic-release/npm",
121 | [
122 | "@semantic-release/git",
123 | {
124 | "assets": [
125 | "package.json",
126 | "CHANGELOG.md"
127 | ],
128 | "message": "chore(release): ${nextRelease.version} semantic"
129 | }
130 | ],
131 | "@semantic-release/github"
132 | ],
133 | "repositoryUrl": "git@github.com:sourcefuse/loopback4-authorization.git"
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/__tests__/acceptance/README.md:
--------------------------------------------------------------------------------
1 | # Acceptance tests
2 |
--------------------------------------------------------------------------------
/src/__tests__/integration/README.md:
--------------------------------------------------------------------------------
1 | # Integration tests
2 |
--------------------------------------------------------------------------------
/src/__tests__/unit/README.md:
--------------------------------------------------------------------------------
1 | # Unit tests
2 |
--------------------------------------------------------------------------------
/src/__tests__/unit/authorization-action.provider.unit.ts:
--------------------------------------------------------------------------------
1 | import {
2 | expect,
3 | ShotRequestOptions,
4 | sinon,
5 | stubExpressContext,
6 | } from '@loopback/testlab';
7 | import {Request} from 'express';
8 | import {AuthorizeActionProvider} from '../../providers';
9 | import {AuthorizationMetadata} from '../../types';
10 | import {mockUser} from './data/mock-user';
11 | import {Context} from '@loopback/core';
12 |
13 | const mockPermissions: AuthorizationMetadata = {
14 | permissions: ['ViewTodo'],
15 | };
16 |
17 | describe('AuthorizeActionProvider', () => {
18 | it('should return true if user has required permissions', async () => {
19 | const metadataGetterStub = sinon.stub().resolves(mockPermissions);
20 | const action = new AuthorizeActionProvider(
21 | metadataGetterStub,
22 | [],
23 | getRequestContext(),
24 | ).value();
25 | const result = await action(mockUser.permissions);
26 | expect(result).to.be.true();
27 | });
28 | it('should return true for all users if resource has "*" as the first permissions', async () => {
29 | const metadataGetterStub = sinon.stub().resolves({permissions: ['*']});
30 | const action = new AuthorizeActionProvider(
31 | metadataGetterStub,
32 | [],
33 | getRequestContext(),
34 | ).value();
35 | const result = await action([]);
36 | expect(result).to.be.true();
37 | });
38 | it('should return false if user does not have required permissions', async () => {
39 | const metadataGetterStub = sinon.stub().resolves(mockPermissions);
40 | const action = new AuthorizeActionProvider(
41 | metadataGetterStub,
42 | [],
43 | getRequestContext(),
44 | ).value();
45 | const result = await action([]);
46 | expect(result).to.be.false();
47 | });
48 | it('should return true if requested resource is in always allowed list', async () => {
49 | const metadataGetterStub = sinon.stub().resolves(mockPermissions);
50 | const action = new AuthorizeActionProvider(
51 | metadataGetterStub,
52 | ['/always/allowed'],
53 | getRequestContext(),
54 | ).value();
55 | const mockRequest = givenRequest({
56 | url: '/always/allowed',
57 | payload: {},
58 | });
59 | const result = await action([], mockRequest);
60 | expect(result).to.be.true();
61 | });
62 | it('should return true if requested resource has a parent path in always allowed list', async () => {
63 | const metadataGetterStub = sinon.stub().resolves(mockPermissions);
64 | const action = new AuthorizeActionProvider(
65 | metadataGetterStub,
66 | ['/always/allowed'],
67 | getRequestContext(),
68 | ).value();
69 | const mockRequest = givenRequest({
70 | url: '/always/allowed/child',
71 | payload: {},
72 | });
73 | const result = await action([], mockRequest);
74 | expect(result).to.be.true();
75 | });
76 | it('should return false if requested resource is not in always allowed list, and user does not have required permissions', async () => {
77 | const metadataGetterStub = sinon.stub().resolves(mockPermissions);
78 | const action = new AuthorizeActionProvider(
79 | metadataGetterStub,
80 | ['/not/always/allowed'],
81 | getRequestContext(),
82 | ).value();
83 | const mockRequest = givenRequest({
84 | url: '/always/allowed',
85 | payload: {},
86 | });
87 | const result = await action([], mockRequest);
88 | expect(result).to.be.false();
89 | });
90 | it('should return false if resource has no attached metadata', async () => {
91 | const requestContext = getRequestContext();
92 | requestContext.get.resolves('');
93 |
94 | const metadataGetterStub = sinon.stub().resolves();
95 | const action = new AuthorizeActionProvider(
96 | metadataGetterStub,
97 | [],
98 | requestContext,
99 | ).value();
100 | const result = await action(mockUser.permissions);
101 | expect(result).to.be.false();
102 | });
103 |
104 | it('should throw 404 if non existing route is hit', async () => {
105 | const requestContext = getRequestContext();
106 | requestContext.get.throws();
107 | const metadataGetterStub = sinon.stub().resolves();
108 | const action = new AuthorizeActionProvider(
109 | metadataGetterStub,
110 | [],
111 | requestContext,
112 | ).value();
113 | const result = await action(mockUser.permissions).catch(err => err);
114 | expect(result).be.instanceOf(Error);
115 | expect(result).to.have.property('message').which.eql('API not found !');
116 | });
117 | function givenRequest(options?: ShotRequestOptions): Request {
118 | return stubExpressContext(options).request;
119 | }
120 | function getRequestContext(): sinon.SinonStubbedInstance {
121 | return sinon.createStubInstance(Context);
122 | }
123 | });
124 |
--------------------------------------------------------------------------------
/src/__tests__/unit/authorization-metadata.provider.unit.ts:
--------------------------------------------------------------------------------
1 | import {expect} from '@loopback/testlab';
2 | import {getAuthorizeMetadata} from '../../providers';
3 | import {AuthorizationMetadata, PermissionObject} from '../../types';
4 | import {authorize} from '../../decorators';
5 | import {get} from '@loopback/rest';
6 |
7 | describe('getAuthorizeMetadata()', function () {
8 | it('should return the authorization metadata when userPermission is provided', () => {
9 | class TestController {
10 | @authorize({permissions: ['default1', 'default2']})
11 | @get('/')
12 | async testMethod() {
13 | // This is intentional.
14 | }
15 | }
16 | const methodName = 'testMethod';
17 | const mockUserPermission: PermissionObject = {
18 | TestController: {
19 | testMethod: ['permission1', 'permission2'],
20 | },
21 | };
22 |
23 | const authorizationMetadata = getAuthorizeMetadata(
24 | TestController,
25 | methodName,
26 | mockUserPermission,
27 | );
28 | expect(authorizationMetadata?.permissions).which.eql(
29 | mockUserPermission.TestController[methodName],
30 | );
31 | });
32 |
33 | it('should return permissions from metadata if userpermission is not provided', () => {
34 | class TestController {
35 | @authorize({permissions: ['default1', 'default2']})
36 | @get('/')
37 | async testMethod() {
38 | // This is intentional.
39 | }
40 | }
41 | const methodName = 'testMethod';
42 | const authorizationMetadata = getAuthorizeMetadata(
43 | TestController,
44 | methodName,
45 | );
46 | const expectedResult: AuthorizationMetadata = {
47 | permissions: ['default1', 'default2'],
48 | };
49 | expect(authorizationMetadata?.permissions).which.eql(
50 | expectedResult.permissions,
51 | );
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/__tests__/unit/casbin-authorization-action.provider.unit.ts:
--------------------------------------------------------------------------------
1 | import {
2 | expect,
3 | ShotRequestOptions,
4 | sinon,
5 | stubExpressContext,
6 | } from '@loopback/testlab';
7 | import * as casbin from 'casbin';
8 | import {StringAdapter} from 'casbin';
9 | import {Request} from 'express';
10 | import {CasbinAuthorizationProvider} from '../../providers';
11 | import {PermissionKeys} from './data';
12 | import {model} from './data/mock-model';
13 | import {policy} from './data/mock-policy';
14 | import {mockUser} from './data/mock-user';
15 |
16 | describe('CasbinAuthorizeActionProvider', () => {
17 | describe('With CasbinPolicy true', () => {
18 | it('should return true if user is authorized according to the policy', async () => {
19 | const configGetter = sinon.stub().resolves(
20 | Promise.resolve({
21 | model: casbin.newModelFromString(model),
22 | policy: new StringAdapter(policy),
23 | }),
24 | );
25 | const casbinMetadataGetter = sinon.stub().resolves({
26 | permissions: [PermissionKeys.ViewTODO],
27 | resource: 'todo',
28 | isCasbinPolicy: true,
29 | });
30 | const action = new CasbinAuthorizationProvider(
31 | casbinMetadataGetter,
32 | configGetter,
33 | [],
34 | ).value();
35 |
36 | const mockRequest = givenRequest({
37 | url: '/',
38 | });
39 | const decision = await action(mockUser, 'todo', mockRequest);
40 | expect(decision).to.be.true();
41 | });
42 |
43 | it('should return false if user is not authorized according to the policy', async () => {
44 | const configGetter = sinon.stub().resolves(
45 | Promise.resolve({
46 | model: casbin.newModelFromString(model),
47 | policy: new StringAdapter(policy),
48 | }),
49 | );
50 | const casbinMetadataGetter = sinon.stub().resolves({
51 | permissions: [PermissionKeys.UpdateTODO],
52 | resource: 'todo',
53 | isCasbinPolicy: true,
54 | });
55 | const action = new CasbinAuthorizationProvider(
56 | casbinMetadataGetter,
57 | configGetter,
58 | [],
59 | ).value();
60 |
61 | const mockRequest = givenRequest({
62 | url: '/',
63 | });
64 | const decision = await action(mockUser, 'todo', mockRequest);
65 | expect(decision).to.be.false();
66 | });
67 |
68 | it('should return true if the resource is always allowed, even if user does not have any permissions', async () => {
69 | const configGetter = sinon.stub().resolves(
70 | Promise.resolve({
71 | model: casbin.newModelFromString(model),
72 | policy: new StringAdapter(policy),
73 | }),
74 | );
75 | const casbinMetadataGetter = sinon.stub().resolves({
76 | permissions: [PermissionKeys.UpdateTODO],
77 | resource: 'todo',
78 | isCasbinPolicy: true,
79 | });
80 | const action = new CasbinAuthorizationProvider(
81 | casbinMetadataGetter,
82 | configGetter,
83 | ['/always/allowed'],
84 | ).value();
85 |
86 | const mockRequest = givenRequest({
87 | url: '/always/allowed',
88 | });
89 | const decision = await action(mockUser, 'todo', mockRequest);
90 | expect(decision).to.be.true();
91 | });
92 |
93 | it('should return true if the resource is always allowed, even if resource has no authorization metadata', async () => {
94 | const configGetter = sinon.stub().resolves(
95 | Promise.resolve({
96 | model: casbin.newModelFromString(model),
97 | policy: new StringAdapter(policy),
98 | }),
99 | );
100 | const casbinMetadataGetter = sinon.stub().resolves();
101 | const action = new CasbinAuthorizationProvider(
102 | casbinMetadataGetter,
103 | configGetter,
104 | ['/always/allowed'],
105 | ).value();
106 |
107 | const mockRequest = givenRequest({
108 | url: '/always/allowed',
109 | });
110 | const decision = await action(mockUser, 'todo', mockRequest);
111 | expect(decision).to.be.true();
112 | });
113 |
114 | it('should return false for a resource with no metadata', async () => {
115 | const configGetter = sinon.stub().resolves(
116 | Promise.resolve({
117 | model: casbin.newModelFromString(model),
118 | policy: new StringAdapter(policy),
119 | }),
120 | );
121 | const casbinMetadataGetter = sinon.stub().resolves();
122 | const action = new CasbinAuthorizationProvider(
123 | casbinMetadataGetter,
124 | configGetter,
125 | [],
126 | ).value();
127 |
128 | const mockRequest = givenRequest({
129 | url: '/',
130 | });
131 | const decision = await action(mockUser, 'todo', mockRequest);
132 | expect(decision).to.be.false();
133 | });
134 |
135 | it('should return true for a resource with "*" as first permission, even when user does not have necessary permissions', async () => {
136 | const configGetter = sinon.stub().resolves(
137 | Promise.resolve({
138 | model: casbin.newModelFromString(model),
139 | policy: new StringAdapter(policy),
140 | }),
141 | );
142 | const casbinMetadataGetter = sinon.stub().resolves({
143 | permissions: ['*'],
144 | resource: 'todo',
145 | isCasbinPolicy: true,
146 | });
147 | const action = new CasbinAuthorizationProvider(
148 | casbinMetadataGetter,
149 | configGetter,
150 | [],
151 | ).value();
152 |
153 | const mockRequest = givenRequest({
154 | url: '/',
155 | });
156 | const decision = await action(mockUser, 'todo', mockRequest);
157 | expect(decision).to.be.true();
158 | });
159 |
160 | it('should throw an error if resource parameter is missing in authorization metadata', async () => {
161 | const configGetter = sinon.stub().resolves(
162 | Promise.resolve({
163 | model: casbin.newModelFromString(model),
164 | policy: new StringAdapter(policy),
165 | }),
166 | );
167 | const casbinMetadataGetter = sinon.stub().resolves({
168 | permissions: [PermissionKeys.UpdateTODO],
169 | isCasbinPolicy: true,
170 | });
171 | const action = new CasbinAuthorizationProvider(
172 | casbinMetadataGetter,
173 | configGetter,
174 | [],
175 | ).value();
176 |
177 | const mockRequest = givenRequest({
178 | url: '/',
179 | });
180 | const decision = action(mockUser, 'todo', mockRequest);
181 |
182 | await expect(decision).to.be.rejectedWith(
183 | `Resource parameter is missing in the decorator.`,
184 | );
185 | });
186 |
187 | it('should throw an error if permissions parameter is missing in authorization metadata', async () => {
188 | const configGetter = sinon.stub().resolves(
189 | Promise.resolve({
190 | model: casbin.newModelFromString(model),
191 | policy: new StringAdapter(policy),
192 | }),
193 | );
194 | const casbinMetadataGetter = sinon.stub().resolves({
195 | resource: 'todo',
196 | isCasbinPolicy: true,
197 | });
198 | const action = new CasbinAuthorizationProvider(
199 | casbinMetadataGetter,
200 | configGetter,
201 | [],
202 | ).value();
203 |
204 | const mockRequest = givenRequest({
205 | url: '/',
206 | });
207 | const decision = action(mockUser, 'todo', mockRequest);
208 |
209 | await expect(decision).to.be.rejectedWith(
210 | `Permissions are missing in the decorator.`,
211 | );
212 | });
213 |
214 | it('should throw an error if user id is missing', async () => {
215 | const configGetter = sinon.stub().resolves(
216 | Promise.resolve({
217 | model: casbin.newModelFromString(model),
218 | policy: new StringAdapter(policy),
219 | }),
220 | );
221 | const casbinMetadataGetter = sinon.stub().resolves({
222 | permissions: [PermissionKeys.UpdateTODO],
223 | resource: 'todo',
224 | isCasbinPolicy: true,
225 | });
226 | const action = new CasbinAuthorizationProvider(
227 | casbinMetadataGetter,
228 | configGetter,
229 | [],
230 | ).value();
231 |
232 | const mockRequest = givenRequest({
233 | url: '/',
234 | });
235 | const decision = action(
236 | {
237 | ...mockUser,
238 | id: undefined,
239 | },
240 | 'todo',
241 | mockRequest,
242 | );
243 |
244 | await expect(decision).to.be.rejectedWith(`User not found.`);
245 | });
246 | });
247 |
248 | describe('With Casbin Policy false', () => {
249 | it('should return true if user is authorized according to the policy', async () => {
250 | const configGetter = sinon.stub().resolves(
251 | Promise.resolve({
252 | model: casbin.newModelFromString(model),
253 | allowedRes: [{resource: 'todo', permission: 'ViewTodo'}],
254 | }),
255 | );
256 |
257 | const casbinMetadataGetter = sinon.stub().resolves({
258 | permissions: [PermissionKeys.ViewTODO],
259 | resource: 'todo',
260 | isCasbinPolicy: false,
261 | });
262 | const action = new CasbinAuthorizationProvider(
263 | casbinMetadataGetter,
264 | configGetter,
265 | [],
266 | ).value();
267 |
268 | const mockRequest = givenRequest({
269 | url: '/',
270 | });
271 | const decision = await action(mockUser, 'todo', mockRequest);
272 | expect(decision).to.be.true();
273 | });
274 |
275 | it('should return false if user is not authorized according to the policy', async () => {
276 | const configGetter = sinon.stub().resolves(
277 | Promise.resolve({
278 | model: casbin.newModelFromString(model),
279 | allowedRes: [{resource: 'todo', permission: 'ViewTodo'}],
280 | }),
281 | );
282 |
283 | const casbinMetadataGetter = sinon.stub().resolves({
284 | permissions: [PermissionKeys.UpdateTODO],
285 | resource: 'todo',
286 | isCasbinPolicy: false,
287 | });
288 | const action = new CasbinAuthorizationProvider(
289 | casbinMetadataGetter,
290 | configGetter,
291 | [],
292 | ).value();
293 |
294 | const mockRequest = givenRequest({
295 | url: '/',
296 | });
297 | const decision = await action(mockUser, 'todo', mockRequest);
298 | expect(decision).to.be.false();
299 | });
300 | });
301 |
302 | function givenRequest(options?: ShotRequestOptions): Request {
303 | return stubExpressContext(options).request;
304 | }
305 | });
306 |
--------------------------------------------------------------------------------
/src/__tests__/unit/data/index.ts:
--------------------------------------------------------------------------------
1 | export * from './permission.enum';
2 |
--------------------------------------------------------------------------------
/src/__tests__/unit/data/mock-model.ts:
--------------------------------------------------------------------------------
1 | export const model = `
2 | [request_definition]
3 | r = sub, obj, act
4 |
5 | [policy_definition]
6 | p = sub, obj, act
7 |
8 | [policy_effect]
9 | e = some(where (p.eft == allow))
10 |
11 | [matchers]
12 | m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
13 | `;
14 |
--------------------------------------------------------------------------------
/src/__tests__/unit/data/mock-policy.ts:
--------------------------------------------------------------------------------
1 | export const policy = `
2 | p, u1, todo, ViewTodo,
3 | `;
4 |
--------------------------------------------------------------------------------
/src/__tests__/unit/data/mock-user.ts:
--------------------------------------------------------------------------------
1 | import {IAuthUserWithPermissions} from '../../../types';
2 |
3 | export const mockUser: IAuthUserWithPermissions = {
4 | id: '1',
5 | role: 'Test',
6 | firstName: 'Test',
7 | lastName: 'User',
8 | username: 'test.user',
9 | authClientId: 0,
10 | permissions: ['ViewTodo'],
11 | };
12 |
--------------------------------------------------------------------------------
/src/__tests__/unit/data/permission.enum.ts:
--------------------------------------------------------------------------------
1 | export enum PermissionKeys {
2 | ViewTODO = 'ViewTodo',
3 | UpdateTODO = 'UpdateTodo',
4 | }
5 |
--------------------------------------------------------------------------------
/src/component.ts:
--------------------------------------------------------------------------------
1 | import {Binding, Component, inject, ProviderMap} from '@loopback/core';
2 |
3 | import {AuthorizationBindings} from './keys';
4 | import {AuthorizeActionProvider} from './providers/authorization-action.provider';
5 | import {AuthorizationMetadataProvider} from './providers/authorization-metadata.provider';
6 | import {CasbinAuthorizationProvider} from './providers/casbin-authorization-action.provider';
7 | import {UserPermissionsProvider} from './providers/user-permissions.provider';
8 | import {AuthorizationConfig} from './types';
9 |
10 | export class AuthorizationComponent implements Component {
11 | providers?: ProviderMap;
12 | bindings?: Binding[] = [];
13 |
14 | constructor(
15 | @inject(AuthorizationBindings.CONFIG)
16 | private readonly config?: AuthorizationConfig,
17 | @inject(AuthorizationBindings.PERMISSION, {optional: true})
18 | private readonly permission?: unknown,
19 | ) {
20 | this.providers = {
21 | [AuthorizationBindings.AUTHORIZE_ACTION.key]: AuthorizeActionProvider,
22 | [AuthorizationBindings.CASBIN_AUTHORIZE_ACTION.key]:
23 | CasbinAuthorizationProvider,
24 | [AuthorizationBindings.METADATA.key]: AuthorizationMetadataProvider,
25 | [AuthorizationBindings.USER_PERMISSIONS.key]: UserPermissionsProvider,
26 | };
27 | if (!this.permission) {
28 | this.bindings?.push(
29 | Binding.bind(AuthorizationBindings.PERMISSION).to(null),
30 | );
31 | }
32 | if (
33 | this.config?.allowAlwaysPaths &&
34 | this.config?.allowAlwaysPaths?.length > 0
35 | ) {
36 | this.bindings?.push(
37 | Binding.bind(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS).to(
38 | this.config.allowAlwaysPaths,
39 | ),
40 | );
41 | } else {
42 | this.bindings?.push(
43 | Binding.bind(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS).to([
44 | '/explorer',
45 | ]),
46 | );
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/controllers/README.md:
--------------------------------------------------------------------------------
1 | # Controllers
2 |
3 | This directory contains source files for the controllers exported by this
4 | extension.
5 |
6 | For more information, see .
7 |
--------------------------------------------------------------------------------
/src/decorators/README.md:
--------------------------------------------------------------------------------
1 | # Decorators
2 |
3 | ## Overview
4 |
5 | Decorators provide annotations for class methods and arguments. Decorators use
6 | the form `@decorator` where `decorator` is the name of the function that will be
7 | called at runtime.
8 |
9 | ## Basic Usage
10 |
11 | ### txIdFromHeader
12 |
13 | This simple decorator allows you to annotate a `Controller` method argument. The
14 | decorator will annotate the method argument with the value of the header
15 | `X-Transaction-Id` from the request.
16 |
17 | **Example**
18 |
19 | ```ts
20 | class MyController {
21 | @get('/')
22 | getHandler(@txIdFromHeader() txId: string) {
23 | return `Your transaction id is: ${txId}`;
24 | }
25 | }
26 | ```
27 |
28 | ## Related Resources
29 |
30 | You can check out the following resource to learn more about decorators and how
31 | they are used in LoopBack Next.
32 |
33 | - [TypeScript Handbook: Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html)
34 | - [Decorators in LoopBack](http://loopback.io/doc/en/lb4/Decorators.html)
35 |
--------------------------------------------------------------------------------
/src/decorators/authorize.decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MetadataInspector,
3 | MethodDecoratorFactory,
4 | Reflector,
5 | } from '@loopback/core';
6 | import {AuthorizationMetadata} from '../types';
7 | import {AUTHORIZATION_METADATA_ACCESSOR} from '../keys';
8 | import {specPreprocessor} from './spec-preprocessor';
9 | import {OperationObject} from '@loopback/rest';
10 |
11 | type OperationMeta = {
12 | verb: string;
13 | path: string;
14 | spec: OperationObject;
15 | };
16 |
17 | const OAI3KEY_METHODS = 'openapi-v3:methods';
18 |
19 | export function authorize(metadata: AuthorizationMetadata) {
20 | const authorizedecorator =
21 | MethodDecoratorFactory.createDecorator(
22 | AUTHORIZATION_METADATA_ACCESSOR,
23 | {
24 | permissions: metadata.permissions || [],
25 | resource: metadata.resource ?? '',
26 | isCasbinPolicy: metadata.isCasbinPolicy ?? false,
27 | },
28 | );
29 | const authorizationWithMetadata = (
30 | target: Object,
31 | propertyKey: string,
32 | descriptor: TypedPropertyDescriptor,
33 | ) => {
34 | const meta: OperationMeta | undefined = MetadataInspector.getMethodMetadata(
35 | OAI3KEY_METHODS,
36 | target,
37 | propertyKey,
38 | );
39 | if (meta) {
40 | meta.spec = specPreprocessor(target, propertyKey, metadata, meta.spec);
41 | Reflector.deleteMetadata(OAI3KEY_METHODS, target, propertyKey);
42 | Reflector.defineMetadata(OAI3KEY_METHODS, meta, target, propertyKey);
43 | authorizedecorator(target, propertyKey, descriptor);
44 | }
45 | };
46 |
47 | return authorizationWithMetadata;
48 | }
49 |
--------------------------------------------------------------------------------
/src/decorators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './authorize.decorator';
2 |
--------------------------------------------------------------------------------
/src/decorators/spec-preprocessor.ts:
--------------------------------------------------------------------------------
1 | import {OperationObject} from 'openapi3-ts';
2 | import {AuthorizationMetadata} from '../types';
3 |
4 | const defaultResponse = (ctor: {name: string}, op: string) => ({
5 | '200': {
6 | description: `Return value of ${ctor.name}.${op}`,
7 | content: {},
8 | },
9 | });
10 |
11 | export const specPreprocessor = (
12 | target: Object,
13 | propertyKey: string,
14 | authorizations: AuthorizationMetadata,
15 | spec?: OperationObject,
16 | ) => {
17 | let desc = spec?.description ?? '';
18 | if (authorizations?.permissions && authorizations?.permissions.length > 0) {
19 | authorizations.permissions
20 | .filter((permission: string) => permission.trim() !== '*')
21 | .forEach((permission, i) => {
22 | if (i === 0) {
23 | desc += `\n\n| Permissions |\n| ------- |\n`;
24 | }
25 | desc += `| ${permission} |\n`;
26 | });
27 | }
28 | if (spec) {
29 | spec.description = desc;
30 | } else {
31 | spec = {
32 | description: desc,
33 | responses: defaultResponse(target.constructor, propertyKey),
34 | } as OperationObject;
35 | }
36 | return spec;
37 | };
38 |
--------------------------------------------------------------------------------
/src/enhancer/spec-description-enhancer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Application,
3 | CoreBindings,
4 | inject,
5 | injectable,
6 | MetadataInspector,
7 | } from '@loopback/core';
8 | import {
9 | asSpecEnhancer,
10 | OASEnhancer,
11 | OpenAPIObject,
12 | OpenApiSpec,
13 | RestEndpoint,
14 | } from '@loopback/rest';
15 |
16 | @injectable(asSpecEnhancer)
17 | export class DescSpecEnhancer implements OASEnhancer {
18 | @inject(CoreBindings.APPLICATION_INSTANCE) private readonly app: Application;
19 | name = 'info';
20 | modifySpec(spec: OpenAPIObject): OpenApiSpec {
21 | for (const controller of this.app.find(`${CoreBindings.CONTROLLERS}.*`)) {
22 | const ctor = controller.valueConstructor;
23 | if (!ctor) {
24 | continue;
25 | }
26 | const endpoints = MetadataInspector.getAllMethodMetadata(
27 | 'openapi-v3:methods',
28 | ctor.prototype,
29 | );
30 | for (const route in endpoints) {
31 | const routeData = endpoints[route];
32 | if (
33 | routeData?.spec?.description &&
34 | !spec.paths[routeData.path][routeData.verb].description
35 | ) {
36 | spec.paths[routeData.path][routeData.verb].description =
37 | routeData.spec.description;
38 | }
39 | }
40 | }
41 | return spec;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/error-keys.ts:
--------------------------------------------------------------------------------
1 | export const enum AuthorizeErrorKeys {
2 | NotAllowedAccess = 'NotAllowedAccess',
3 | }
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './component';
2 | export * from './types';
3 | export * from './keys';
4 | export * from './error-keys';
5 | export * from './decorators';
6 | export * from './providers';
7 |
--------------------------------------------------------------------------------
/src/keys.ts:
--------------------------------------------------------------------------------
1 | import {BindingKey} from '@loopback/context';
2 | import {MetadataAccessor} from '@loopback/metadata';
3 | import {
4 | AuthorizeFn,
5 | AuthorizationMetadata,
6 | UserPermissionsFn,
7 | AuthorizationConfig,
8 | CasbinAuthorizeFn,
9 | CasbinEnforcerConfigGetterFn,
10 | CasbinResourceModifierFn,
11 | PermissionObject,
12 | } from './types';
13 |
14 | /**
15 | * Binding keys used by this component.
16 | */
17 | export namespace AuthorizationBindings {
18 | export const AUTHORIZE_ACTION = BindingKey.create(
19 | 'sf.userAuthorization.actions.authorize',
20 | );
21 | export const PERMISSION = BindingKey.create(
22 | `sf.userAuthorization.authorize.permissions`,
23 | );
24 |
25 | export const CASBIN_AUTHORIZE_ACTION = BindingKey.create(
26 | 'sf.userAuthorization.actions.casbin.authorize',
27 | );
28 |
29 | export const METADATA = BindingKey.create(
30 | 'sf.userAuthorization.operationMetadata',
31 | );
32 |
33 | export const USER_PERMISSIONS = BindingKey.create>(
34 | 'sf.userAuthorization.actions.userPermissions',
35 | );
36 |
37 | export const CASBIN_ENFORCER_CONFIG_GETTER =
38 | BindingKey.create(
39 | 'sf.userAuthorization.actions.casbin.config',
40 | );
41 |
42 | export const CASBIN_RESOURCE_MODIFIER_FN =
43 | BindingKey.create(
44 | 'sf.userAuthorization.actions.casbin.resourceModifier',
45 | );
46 |
47 | export const CONFIG = BindingKey.create(
48 | 'sf.userAuthorization.config',
49 | );
50 |
51 | export const PATHS_TO_ALLOW_ALWAYS = 'sf.userAuthorization.allowAlways';
52 | }
53 |
54 | export const AUTHORIZATION_METADATA_ACCESSOR = MetadataAccessor.create<
55 | AuthorizationMetadata,
56 | MethodDecorator
57 | >('sf.userAuthorization.accessor.operationMetadata');
58 |
--------------------------------------------------------------------------------
/src/mixins/README.md:
--------------------------------------------------------------------------------
1 | # Mixins
2 |
3 | This directory contains source files for the mixins exported by this extension.
4 |
5 | ## Overview
6 |
7 | Sometimes it's helpful to write partial classes and then combining them together
8 | to build more powerful classes. This pattern is called Mixins (mixing in partial
9 | classes) and is supported by LoopBack 4.
10 |
11 | LoopBack 4 supports mixins at an `Application` level. Your partial class can
12 | then be mixed into the `Application` class. A mixin class can modify or override
13 | existing methods of the class or add new ones! It is also possible to mixin
14 | multiple classes together as needed.
15 |
16 | ### High level example
17 |
18 | ```ts
19 | class MyApplication extends MyMixinClass(Application) {
20 | // Your code
21 | }
22 |
23 | // Multiple Classes mixed together
24 | class MyApp extends MyMixinClass(MyMixinClass2(Application)) {
25 | // Your code
26 | }
27 | ```
28 |
29 | ## Getting Started
30 |
31 | For hello-extensions we write a simple Mixin that allows the `Application` class
32 | to bind a `Logger` class from ApplicationOptions, Components, or `.logger()`
33 | method that is mixed in. `Logger` instances are bound to the key
34 | `loggers.${Logger.name}`. Once a Logger has been bound, the user can retrieve it
35 | by using
36 | [Dependency Injection](http://loopback.io/doc/en/lb4/Dependency-injection.html)
37 | and the key for the `Logger`.
38 |
39 | ### What is a Logger?
40 |
41 | > A Logger class is provides a mechanism for logging messages of varying
42 | > priority by providing an implementation for `Logger.info()` &
43 | > `Logger.error()`. An example of a Logger is `console` which has
44 | > `console.log()` and `console.error()`.
45 |
46 | #### An example Logger
47 |
48 | ```ts
49 | class ColorLogger implements Logger {
50 | log(...args: LogArgs) {
51 | console.log('log :', ...args);
52 | }
53 |
54 | error(...args: LogArgs) {
55 | // log in red color
56 | console.log('\x1b[31m error: ', ...args, '\x1b[0m');
57 | }
58 | }
59 | ```
60 |
61 | ## LoggerMixin
62 |
63 | A complete & functional implementation can be found in `logger.mixin.ts`. _Here
64 | are some key things to keep in mind when writing your own Mixin_.
65 |
66 | ### constructor()
67 |
68 | A Mixin constructor must take an array of any type as it's argument. This would
69 | represent `ApplicationOptions` for our base class `Application` as well as any
70 | properties we would like for our Mixin.
71 |
72 | It is also important for the constructor to call `super(args)` so `Application`
73 | continues to work as expected.
74 |
75 | ```ts
76 | constructor(...args: any[]) {
77 | super(args);
78 | }
79 | ```
80 |
81 | ### Binding via `ApplicationOptions`
82 |
83 | As mentioned earlier, since our `args` represents `ApplicationOptions`, we can
84 | make it possible for users to pass in their `Logger` implementations in a
85 | `loggers` array on `ApplicationOptions`. We can then read the array and
86 | automatically bind these for the user.
87 |
88 | #### Example user experience
89 |
90 | ```ts
91 | class MyApp extends LoggerMixin(Application) {
92 | constructor(...args: any[]) {
93 | super(...args);
94 | }
95 | }
96 |
97 | const app = new MyApp({
98 | loggers: [ColorLogger],
99 | });
100 | ```
101 |
102 | #### Example Implementation
103 |
104 | To implement this, we would check `this.options` to see if it has a `loggers`
105 | array and if so, bind it by calling the `.logger()` method. (More on that
106 | below).
107 |
108 | ```ts
109 | if (this.options.loggers) {
110 | for (const logger of this.options.loggers) {
111 | this.logger(logger);
112 | }
113 | }
114 | ```
115 |
116 | ### Binding via `.logger()`
117 |
118 | As mentioned earlier, we can add a new function to our `Application` class
119 | called `.logger()` into which a user would pass in their `Logger` implementation
120 | so we can bind it to the `loggers.*` key for them. We just add this new method
121 | on our partial Mixin class.
122 |
123 | ```ts
124 | logger(logClass: Logger) {
125 | const loggerKey = `loggers.${logClass.name}`;
126 | this.bind(loggerKey).toClass(logClass);
127 | }
128 | ```
129 |
130 | ### Binding a `Logger` from a `Component`
131 |
132 | Our base class of `Application` already has a method that binds components. We
133 | can modify this method to continue binding a `Component` as usual but also
134 | binding any `Logger` instances provided by that `Component`. When modifying
135 | behavior of an existing method, we can ensure existing behavior by calling the
136 | `super.method()`. In our case the method is `.component()`.
137 |
138 | ```ts
139 | component(component: Constructor) {
140 | super.component(component); // ensures existing behavior from Application
141 | this.mountComponentLoggers(component);
142 | }
143 | ```
144 |
145 | We have now modified `.component()` to do it's thing and then call our method
146 | `mountComponentLoggers()`. In this method is where we check for `Logger`
147 | implementations declared by the component in a `loggers` array by retrieving the
148 | instance of the `Component`. Then if `loggers` array exists, we bind the
149 | `Logger` instances as normal (by leveraging our `.logger()` method).
150 |
151 | ```ts
152 | mountComponentLoggers(component: Constructor) {
153 | const componentKey = `components.${component.name}`;
154 | const compInstance = this.getSync(componentKey);
155 |
156 | if (compInstance.loggers) {
157 | for (const logger of compInstance.loggers) {
158 | this.logger(logger);
159 | }
160 | }
161 | }
162 | ```
163 |
164 | ## Retrieving the Logger instance
165 |
166 | Now that we have bound a Logger to our Application via one of the many ways made
167 | possible by `LoggerMixin`, we need to be able to retrieve it so we can use it.
168 | Let's say we want to use it in a controller. Here's an example to retrieving it
169 | so we can use it.
170 |
171 | ```ts
172 | class MyController {
173 | constructor(@inject('loggers.ColorLogger') protected log: Logger) {}
174 |
175 | helloWorld() {
176 | this.log.log('hello log');
177 | this.log.error('hello error');
178 | }
179 | }
180 | ```
181 |
182 | ## Examples for using LoggerMixin
183 |
184 | ### Using the app's `.logger()` method
185 |
186 | ```ts
187 | class LoggingApplication extends LoggerMixin(Application) {
188 | constructor(...args: any[]) {
189 | super(...args);
190 | this.logger(ColorLogger);
191 | }
192 | }
193 | ```
194 |
195 | ### Using the app's constructor
196 |
197 | ```ts
198 | class LoggerApplication extends LoggerMixin(Application) {
199 | constructor() {
200 | super({
201 | loggers: [ColorLogger],
202 | });
203 | }
204 | }
205 | ```
206 |
207 | ### Binding a Logger provided by a component
208 |
209 | ```ts
210 | class LoggingComponent implements Component {
211 | loggers: [ColorLogger];
212 | }
213 |
214 | const app = new LoggingApplication();
215 | app.component(LoggingComponent); // Logger from MyComponent will be bound to loggers.ColorLogger
216 | ```
217 |
--------------------------------------------------------------------------------
/src/policy.csv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sourcefuse/loopback4-authorization/847e04d1edf97b04135f97fd14fc070d7985fbfc/src/policy.csv
--------------------------------------------------------------------------------
/src/providers/README.md:
--------------------------------------------------------------------------------
1 | # Providers
2 |
3 | This directory contains providers contributing additional bindings, for example
4 | custom sequence actions.
5 |
6 | ## Overview
7 |
8 | A [provider](http://loopback.io/doc/en/lb4/Creating-components.html#providers)
9 | is a class that provides a `value()` function. This function is called `Context`
10 | when another entity requests a value to be injected.
11 |
12 | Here we create a provider for a logging function that can be used as a new
13 | action in a custom [sequence](http://loopback.io/doc/en/lb4/Sequence.html).
14 |
15 | The logger will log the URL, the parsed request parameters, and the result. The
16 | logger is also capable of timing the sequence if you start a timer at the start
17 | of the sequence using `this.logger.startTimer()`.
18 |
19 | ## Basic Usage
20 |
21 | ### TimerProvider
22 |
23 | TimerProvider is automatically bound to your Application's
24 | [Context](http://loopback.io/doc/en/lb4/Context.html) using the LogComponent
25 | which exports this provider with a binding key of `extension-starter.timer`. You
26 | can learn more about components in the
27 | [related resources section](#related-resources).
28 |
29 | This provider makes availble to your application a timer function which given a
30 | start time _(given as an array [seconds, nanoseconds])_ can give you a total
31 | time elapsed since the start in milliseconds. The timer can also start timing if
32 | no start time is given. This is used by LogComponent to allow a user to time a
33 | Sequence.
34 |
35 | _NOTE:_ _You can get the start time in the required format by using
36 | `this.logger.startTimer()`._
37 |
38 | You can provide your own implementation of the elapsed time function by binding
39 | it to the binding key (accessible via `ExtensionStarterBindings`) as follows:
40 |
41 | ```ts
42 | app.bind(ExtensionStarterBindings.TIMER).to(timerFn);
43 | ```
44 |
45 | ### LogProvider
46 |
47 | LogProvider can automatically be bound to your Application's Context using the
48 | LogComponent which exports the provider with a binding key of
49 | `extension-starter.actions.log`.
50 |
51 | The key can be accessed by importing `ExtensionStarterBindings` as follows:
52 |
53 | **Example: Binding Keys**
54 |
55 | ```ts
56 | import {ExtensionStarterBindings} from 'HelloExtensions';
57 | // Key can be accessed as follows now
58 | const key = ExtensionStarterBindings.LOG_ACTION;
59 | ```
60 |
61 | LogProvider gives us a seuqence action and a `startTimer` function. In order to
62 | use the sequence action, you must define your own sequence as shown below.
63 |
64 | **Example: Sequence**
65 |
66 | ```ts
67 | class LogSequence implements SequenceHandler {
68 | constructor(
69 | @inject(coreSequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
70 | @inject(coreSequenceActions.PARSE_PARAMS)
71 | protected parseParams: ParseParams,
72 | @inject(coreSequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
73 | @inject(coreSequenceActions.SEND) protected send: Send,
74 | @inject(coreSequenceActions.REJECT) protected reject: Reject,
75 | // We get the logger injected by the LogProvider here
76 | @inject(ExtensionStarterBindings.LOG_ACTION) protected logger: LogFn,
77 | ) {}
78 |
79 | async handle(context: RequestContext) {
80 | const {request, response} = context;
81 |
82 | // We define these variable outside so they can be accessed by logger.
83 | let args: any = [];
84 | let result: any;
85 |
86 | // Optionally start timing the sequence using the timer
87 | // function available via LogFn
88 | const start = this.logger.startTimer();
89 |
90 | try {
91 | const route = this.findRoute(request);
92 | args = await this.parseParams(request, route);
93 | result = await this.invoke(route, args);
94 | this.send(response, result);
95 | } catch (error) {
96 | result = error; // so we can log the error message in the logger
97 | this.reject(context, error);
98 | }
99 |
100 | // We call the logger function given to us by LogProvider
101 | this.logger(request, args, result, start);
102 | }
103 | }
104 | ```
105 |
106 | Once a sequence has been written, we can just use that in our Application as
107 | follows:
108 |
109 | **Example: Application**
110 |
111 | ```ts
112 | const app = new Application({
113 | sequence: LogSequence,
114 | });
115 | app.component(LogComponent);
116 |
117 | // Now all requests handled by our sequence will be logged.
118 | ```
119 |
120 | ## Related Resources
121 |
122 | You can check out the following resource to learn more about providers,
123 | components, sequences, and binding keys.
124 |
125 | - [Providers](http://loopback.io/doc/en/lb4/Creating-components.html#providers)
126 | - [Creating Components](http://loopback.io/doc/en/lb4/Creating-components.html)
127 | - [Using Components](http://loopback.io/doc/en/lb4/Using-components.html)
128 | - [Sequence](http://loopback.io/doc/en/lb4/Sequence.html)
129 | - [Binding Keys](http://loopback.io/doc/en/lb4/Decorators.html)
130 |
--------------------------------------------------------------------------------
/src/providers/authorization-action.provider.ts:
--------------------------------------------------------------------------------
1 | import {Getter, inject, Provider} from '@loopback/context';
2 |
3 | import {AuthorizationBindings} from '../keys';
4 | import {AuthorizationMetadata, AuthorizeFn} from '../types';
5 |
6 | import {intersection} from 'lodash';
7 | import {Request} from 'express';
8 | import {HttpErrors, RestBindings} from '@loopback/rest';
9 | import {CoreBindings, Context} from '@loopback/core';
10 |
11 | export class AuthorizeActionProvider implements Provider {
12 | constructor(
13 | @inject.getter(AuthorizationBindings.METADATA)
14 | private readonly getMetadata: Getter,
15 | @inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)
16 | private readonly allowAlwaysPath: string[],
17 | @inject(RestBindings.Http.CONTEXT)
18 | private readonly requestContext: Context,
19 | ) {}
20 |
21 | value(): AuthorizeFn {
22 | return (response, req) => this.action(response, req);
23 | }
24 |
25 | async action(userPermissions: string[], request?: Request): Promise {
26 | const metadata: AuthorizationMetadata = await this.getMetadata();
27 |
28 | if (request && this.checkIfAllowedAlways(request)) {
29 | return true;
30 | }
31 |
32 | if (metadata) {
33 | if (metadata.permissions.indexOf('*') === 0) {
34 | // Return immediately with true, if allowed to all
35 | // This is for publicly open routes only
36 | return true;
37 | }
38 | } else {
39 | try {
40 | await this.requestContext.get(CoreBindings.CONTROLLER_METHOD_NAME);
41 | return false;
42 | } catch (error) {
43 | throw new HttpErrors.NotFound('API not found !');
44 | }
45 | }
46 |
47 | const permissionsToCheck = metadata.permissions;
48 | return intersection(userPermissions, permissionsToCheck).length > 0;
49 | }
50 |
51 | checkIfAllowedAlways(req: Request): boolean {
52 | let allowed = false;
53 | allowed = !!this.allowAlwaysPath.find(path => req.path.indexOf(path) === 0);
54 | return allowed;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/providers/authorization-metadata.provider.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Constructor,
3 | inject,
4 | MetadataInspector,
5 | Provider,
6 | } from '@loopback/context';
7 | import {CoreBindings} from '@loopback/core';
8 |
9 | import {AUTHORIZATION_METADATA_ACCESSOR, AuthorizationBindings} from '../keys';
10 | import {AuthorizationMetadata, PermissionObject} from '../types';
11 |
12 | export class AuthorizationMetadataProvider
13 | implements Provider
14 | {
15 | constructor(
16 | @inject(CoreBindings.CONTROLLER_CLASS, {optional: true})
17 | private readonly controllerClass: Constructor<{}>,
18 | @inject(CoreBindings.CONTROLLER_METHOD_NAME, {optional: true})
19 | private readonly methodName: string,
20 | @inject(AuthorizationBindings.PERMISSION)
21 | private permissionObject: PermissionObject,
22 | ) {}
23 |
24 | value(): AuthorizationMetadata | undefined {
25 | if (!this.controllerClass || !this.methodName) return;
26 | return getAuthorizeMetadata(
27 | this.controllerClass,
28 | this.methodName,
29 | this.permissionObject,
30 | );
31 | }
32 | }
33 |
34 | export function getAuthorizeMetadata(
35 | controllerClass: Constructor<{}>,
36 | methodName: string,
37 | userPermission?: PermissionObject,
38 | ): AuthorizationMetadata | undefined {
39 | const authorizationMetadata =
40 | MetadataInspector.getMethodMetadata(
41 | AUTHORIZATION_METADATA_ACCESSOR,
42 | controllerClass.prototype,
43 | methodName,
44 | ) ?? {permissions: []};
45 | if (userPermission) {
46 | const methodPermissions =
47 | userPermission?.[controllerClass.name]?.[methodName];
48 | if (methodPermissions) {
49 | authorizationMetadata.permissions = methodPermissions;
50 | }
51 | }
52 | return authorizationMetadata;
53 | }
54 |
--------------------------------------------------------------------------------
/src/providers/casbin-authorization-action.provider.ts:
--------------------------------------------------------------------------------
1 | import {Getter, inject, Provider} from '@loopback/core';
2 | import {Request} from '@loopback/express';
3 | import {HttpErrors} from '@loopback/rest';
4 | import * as casbin from 'casbin';
5 | import {AuthorizationBindings} from '../keys';
6 | import {
7 | AuthorizationMetadata,
8 | CasbinAuthorizeFn,
9 | CasbinEnforcerConfigGetterFn,
10 | IAuthUserWithPermissions,
11 | ResourcePermissionObject,
12 | } from '../types';
13 |
14 | export class CasbinAuthorizationProvider
15 | implements Provider
16 | {
17 | constructor(
18 | @inject.getter(AuthorizationBindings.METADATA)
19 | private readonly getCasbinMetadata: Getter,
20 | @inject(AuthorizationBindings.CASBIN_ENFORCER_CONFIG_GETTER)
21 | private readonly getCasbinEnforcerConfig: CasbinEnforcerConfigGetterFn,
22 | @inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)
23 | private readonly allowAlwaysPath: string[],
24 | ) {}
25 |
26 | value(): CasbinAuthorizeFn {
27 | return (response, resource, request) =>
28 | this.action(response, resource, request);
29 | }
30 |
31 | async action(
32 | user: IAuthUserWithPermissions,
33 | resource: string,
34 | request?: Request,
35 | ): Promise {
36 | let authDecision = false;
37 | try {
38 | // fetch decorator metadata
39 | const metadata: AuthorizationMetadata = await this.getCasbinMetadata();
40 |
41 | if (request && this.checkIfAllowedAlways(request)) {
42 | return true;
43 | }
44 |
45 | if (metadata?.permissions?.indexOf('*') === 0) {
46 | // Return immediately with true, if allowed to all
47 | // This is for publicly open routes only
48 | return true;
49 | }
50 |
51 | if (!metadata?.resource) {
52 | if (!metadata) {
53 | return false;
54 | }
55 | throw new HttpErrors.Unauthorized(
56 | `Resource parameter is missing in the decorator.`,
57 | );
58 | }
59 |
60 | if (!user.id) {
61 | throw new HttpErrors.Unauthorized(`User not found.`);
62 | }
63 | const subject = this.getUserName(`${user.id}`);
64 |
65 | const desiredPermissions = this.getDesiredPermissions(metadata);
66 |
67 | // Fetch casbin config by invoking casbin-config-getter-provider
68 | const casbinConfig = await this.getCasbinEnforcerConfig(
69 | user,
70 | metadata.resource,
71 | metadata.isCasbinPolicy,
72 | );
73 |
74 | let enforcer: casbin.Enforcer;
75 |
76 | // If casbin config policy format is being used, create enforcer
77 | if (metadata.isCasbinPolicy) {
78 | enforcer = await casbin.newEnforcer(
79 | casbinConfig.model,
80 | casbinConfig.policy,
81 | );
82 | }
83 | // In case casbin policy is coming via provider, use that to initialise enforcer
84 | else if (casbinConfig.allowedRes) {
85 | const policy = this.createCasbinPolicy(
86 | casbinConfig.allowedRes,
87 | subject,
88 | );
89 | const stringAdapter = new casbin.StringAdapter(policy);
90 | enforcer = new casbin.Enforcer();
91 | await enforcer.initWithModelAndAdapter(
92 | casbinConfig.model as casbin.Model,
93 | stringAdapter,
94 | );
95 | } else {
96 | return false;
97 | }
98 |
99 | // Use casbin enforce method to get authorization decision
100 | for (const permission of desiredPermissions) {
101 | const decision = await enforcer.enforce(subject, resource, permission);
102 | authDecision = authDecision || decision;
103 | }
104 | } catch (err) {
105 | throw new HttpErrors.Unauthorized(err.message);
106 | }
107 |
108 | return authDecision;
109 | }
110 |
111 | // Generate the user name according to the naming convention
112 | // in casbin policy
113 | // A user's name would be `u${ id }`
114 | getUserName(id: string): string {
115 | return `u${id}`;
116 | }
117 |
118 | getDesiredPermissions(metadata: AuthorizationMetadata): Array {
119 | if (metadata.permissions && metadata.permissions.length > 0) {
120 | return metadata.permissions;
121 | } else {
122 | throw new HttpErrors.Unauthorized(
123 | `Permissions are missing in the decorator.`,
124 | );
125 | }
126 | }
127 |
128 | // Create casbin policy for user based on ResourcePermission data provided by extension client
129 | createCasbinPolicy(
130 | resPermObj: ResourcePermissionObject[],
131 | subject: string,
132 | ): string {
133 | let result = '';
134 | resPermObj.forEach(resPerm => {
135 | const policy = `p, ${subject}, ${resPerm.resource}, ${resPerm.permission}
136 | `;
137 | result += policy;
138 | });
139 |
140 | return result;
141 | }
142 |
143 | checkIfAllowedAlways(req: Request): boolean {
144 | let allowed = false;
145 | allowed = !!this.allowAlwaysPath.find(path => req.path.indexOf(path) === 0);
146 | return allowed;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/providers/casbin-enforcer-config.provider.ts:
--------------------------------------------------------------------------------
1 | import {Provider} from '@loopback/context';
2 |
3 | import {HttpErrors} from '@loopback/rest';
4 | import {CasbinEnforcerConfigGetterFn, IAuthUserWithPermissions} from '../types';
5 |
6 | export class CasbinEnforcerProvider
7 | implements Provider
8 | {
9 | value(): CasbinEnforcerConfigGetterFn {
10 | return async (
11 | authUser: IAuthUserWithPermissions,
12 | resource: string,
13 | isCasbinPolicy?: boolean,
14 | ) => {
15 | throw new HttpErrors.NotImplemented(
16 | `CasbinEnforcerConfigGetterFn Provider is not implemented`,
17 | );
18 | };
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/providers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './authorization-metadata.provider';
2 | export * from './authorization-action.provider';
3 | export * from './user-permissions.provider';
4 | export * from './casbin-authorization-action.provider';
5 | export * from './casbin-enforcer-config.provider';
6 |
--------------------------------------------------------------------------------
/src/providers/user-permissions.provider.ts:
--------------------------------------------------------------------------------
1 | import {Provider} from '@loopback/context';
2 |
3 | import {UserPermission, UserPermissionsFn} from '../types';
4 |
5 | export class UserPermissionsProvider
6 | implements Provider>
7 | {
8 | value(): UserPermissionsFn {
9 | return (userPermissions, rolePermissions) =>
10 | this.action(userPermissions, rolePermissions);
11 | }
12 |
13 | action(
14 | userPermissions: UserPermission[],
15 | rolePermissions: string[],
16 | ): string[] {
17 | let perms: string[] = [];
18 | // First add all permissions associated with role
19 | perms = perms.concat(rolePermissions);
20 | // Now update permissions based on user permissions
21 | userPermissions.forEach((userPerm: UserPermission) => {
22 | if (userPerm.allowed && perms.indexOf(userPerm.permission) < 0) {
23 | // Add permission if it is not part of role but allowed to user
24 | perms.push(userPerm.permission);
25 | } else if (!userPerm.allowed && perms.indexOf(userPerm.permission) >= 0) {
26 | // Remove permission if it is disallowed for user
27 | perms.splice(perms.indexOf(userPerm.permission), 1);
28 | } else {
29 | //this is intentional
30 | }
31 | });
32 | return perms;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/release_notes/mymarkdown.ejs:
--------------------------------------------------------------------------------
1 | ## Release [<%= range.split('..')[1] %>](https://github.com/sourcefuse/loopback4-authorization/compare/<%= range %>) <%= new Date().toLocaleDateString('en-us', {year:"numeric", month:"long", day:"numeric"})
2 | ;%>
3 | Welcome to the <%= new Date().toLocaleDateString('en-us', {year:"numeric", month:"long", day:"numeric"});%> release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
4 | <% commits.forEach(function (commit) { %>
5 | - [<%= commit.issueTitle %>](https://github.com/sourcefuse/loopback4-authorization/issues/<%= commit.issueno %>) :- [<%= commit.title %>](https://github.com/sourcefuse/loopback4-authorization/commit/<%= commit.sha1%>) was commited on <%= commit.committerDate %> by [<%= commit.authorName %>](mailto:<%= commit.authorEmail %>)
6 | <% commit.messageLines.forEach(function (message) { %>
7 | - <%= message %>
8 | <% }) %>
9 | <% }) %>
10 | Clink on the above links to understand the changes in detail.
11 | ___
12 |
13 |
--------------------------------------------------------------------------------
/src/release_notes/post-processing.js:
--------------------------------------------------------------------------------
1 | const https = require('node:https');
2 | const jsdom = require('jsdom');
3 | module.exports = async function (data, callback) {
4 | const rewritten = [];
5 | for (const commit of data.commits) {
6 | if (commit.title.indexOf('chore(release)') !== -1) {
7 | continue;
8 | }
9 |
10 | const commitTitle = commit.title;
11 | commit.title = commitTitle.substring(0, commitTitle.indexOf('#') - 1);
12 |
13 | commit.messageLines = commit.messageLines.filter(message => {
14 | if (message.indexOf('efs/') === -1) return message;
15 | });
16 |
17 | commit.messageLines.forEach(message => {
18 | commit.issueno = message.includes('GH-')
19 | ? message.replace('GH-', '').trim()
20 | : null;
21 | });
22 |
23 | const issueDesc = await getIssueDesc(commit.issueno).then(res => {
24 | return res;
25 | });
26 | commit.issueTitle = issueDesc;
27 |
28 | commit.committerDate = new Date(commit.committerDate).toLocaleDateString(
29 | 'en-us',
30 | {
31 | year: 'numeric',
32 | month: 'long',
33 | day: 'numeric',
34 | },
35 | );
36 | rewritten.push(commit);
37 | }
38 | callback({
39 | commits: rewritten.filter(Boolean),
40 | range: data.range,
41 | });
42 | };
43 |
44 | function getIssueDesc(issueNo) {
45 | return new Promise((resolve, reject) => {
46 | let result = '';
47 | const req = https.get(
48 | `https://github.com/sourcefuse/loopback4-authorization/issues/${encodeURIComponent(
49 | issueNo,
50 | )}`,
51 | res => {
52 | res.setEncoding('utf8');
53 | res.on('data', chunk => {
54 | result = result + chunk;
55 | });
56 | res.on('end', () => {
57 | const {JSDOM} = jsdom;
58 | const dom = new JSDOM(result);
59 | const title = dom.window.document.getElementsByClassName(
60 | 'js-issue-title markdown-title',
61 | );
62 | let issueTitle = '';
63 | for (const ele of title) {
64 | if (ele.nodeName === 'BDI') {
65 | issueTitle = ele.innerHTML;
66 | }
67 | }
68 | resolve(issueTitle);
69 | });
70 | },
71 | );
72 | req.on('error', e => {
73 | reject(e);
74 | });
75 | req.end();
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/src/release_notes/release-notes.js:
--------------------------------------------------------------------------------
1 | const releaseNotes = require('git-release-notes');
2 | const simpleGit = require('simple-git/promise');
3 | const path = require('path');
4 | const {readFile, writeFile, ensureFile} = require('fs-extra');
5 |
6 | async function generateReleaseNotes() {
7 | try {
8 | const OPTIONS = {
9 | branch: 'master',
10 | s: './post-processing.js',
11 | };
12 | const RANGE = await getRange();
13 | const TEMPLATE = './mymarkdown.ejs';
14 |
15 | const changelog = await releaseNotes(OPTIONS, RANGE, TEMPLATE);
16 |
17 | const changelogPath = path.resolve(__dirname, '../..', 'CHANGELOG.md');
18 | await ensureFile(changelogPath);
19 | const currentFile = (await readFile(changelogPath)).toString().trim();
20 | if (currentFile) {
21 | console.log('Update %s', changelogPath);
22 | } else {
23 | console.log('Create %s', changelogPath);
24 | }
25 |
26 | await writeFile(changelogPath, changelog);
27 | await writeFile(changelogPath, currentFile, {flag: 'a+'});
28 | await addAndCommit().then(() => {
29 | console.log('Changelog has been updated');
30 | });
31 | } catch (ex) {
32 | console.error(ex);
33 | process.exit(1);
34 | }
35 | }
36 |
37 | async function getRange() {
38 | const git = simpleGit();
39 | const tags = (await git.tag({'--sort': 'committerdate'})).split('\n');
40 | tags.pop();
41 |
42 | const startTag = tags.slice(-2)[0];
43 | const endTag = tags.slice(-1)[0];
44 | return `${startTag}..${endTag}`;
45 | }
46 |
47 | async function addAndCommit() {
48 | const git = simpleGit();
49 | await git.add(['../../CHANGELOG.md']);
50 | await git.commit('chore(release): changelog file', {
51 | '--no-verify': null,
52 | });
53 | await git.push('origin', 'master');
54 | }
55 |
56 | generateReleaseNotes().catch(ex => {
57 | console.error(ex);
58 | process.exit(1);
59 | });
60 |
--------------------------------------------------------------------------------
/src/repositories/README.md:
--------------------------------------------------------------------------------
1 | # Repositories
2 |
3 | This directory contains code for repositories provided by this extension.
4 |
5 | For more information, see .
6 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import {Request} from '@loopback/express';
2 | import {FileAdapter, Model} from 'casbin';
3 |
4 | import PostgresAdapter from 'casbin-pg-adapter';
5 |
6 | /**
7 | * Authorize action method type
8 | *
9 | * @param userPermissions - Array of permission keys granted to the user
10 | * This is actually a union of permissions picked up based on role
11 | * attached to the user and allowed permissions at specific user level
12 | */
13 | export type AuthorizeFn = (
14 | userPermissions: string[],
15 | request?: Request,
16 | ) => Promise;
17 |
18 | /**
19 | * Casbin authorize action method type
20 | * @param user - User object corresponding to the logged in user
21 | * @param resVal - value of the resource for which authorisation is being sought
22 | *
23 | */
24 | export type CasbinAuthorizeFn = (
25 | user: IAuthUserWithPermissions,
26 | resVal: string,
27 | request: Request,
28 | ) => Promise;
29 |
30 | export type PermissionObject = {
31 | [controller: string]: {
32 | [method: string]: string[];
33 | };
34 | };
35 | /**
36 | * Authorization metadata interface for the method decorator
37 | */
38 | export interface AuthorizationMetadata {
39 | // Array of permissions required at the method level.
40 | // User need to have at least one of these to access the API method.
41 | permissions: string[];
42 |
43 | // Name of resource for which authorisation is being sought
44 | resource?: string;
45 |
46 | /**
47 | * Boolean flag to determine whether we are using casbin policy format or not
48 | * isCasbinPolicy = true, when we are providing casbin format policy doc in application
49 | * isCasbinPolicy = false, when we are implementing provider in app to give casbin policy
50 | */
51 | isCasbinPolicy?: boolean;
52 | }
53 |
54 | /**
55 | * Authorization config type for providing config to the component
56 | */
57 | export interface AuthorizationConfig {
58 | /**
59 | * Specify paths to always allow. No permissions check needed.
60 | */
61 | allowAlwaysPaths: string[];
62 | }
63 |
64 | /**
65 | * Permissions interface to be implemented by models
66 | */
67 | export interface Permissions {
68 | permissions: T[];
69 | }
70 |
71 | /**
72 | * Override permissions at user level
73 | */
74 | export interface UserPermissionsOverride {
75 | permissions: UserPermission[];
76 | }
77 |
78 | /**
79 | * User Permission model
80 | * used for explicit allow/deny any permission at user level
81 | */
82 | export interface UserPermission {
83 | permission: T;
84 | allowed: boolean;
85 | }
86 |
87 | /**
88 | * User permissions manipulation method type.
89 | *
90 | * This is where we can add our business logic to read and
91 | * union permissions associated to user via role with
92 | * those associated directly to the user.
93 | *
94 | */
95 | export type UserPermissionsFn = (
96 | userPermissions: UserPermission[],
97 | rolePermissions: T[],
98 | ) => T[];
99 |
100 | /**
101 | * Casbin enforcer getter method type
102 | *
103 | * This method provides the Casbin config
104 | * required to initialise a Casbin enforcer
105 | */
106 | export type CasbinEnforcerConfigGetterFn = (
107 | authUser: IAuthUserWithPermissions,
108 | resource: string,
109 | isCasbinPolicy?: boolean,
110 | ) => Promise;
111 |
112 | /**
113 | * Casbin resource value modifier method type
114 | *
115 | * This method can help modify the resource value
116 | * for integration with casbin, as per business logic
117 | */
118 | export type CasbinResourceModifierFn = (
119 | pathParams: string[],
120 | req: Request,
121 | ) => Promise;
122 |
123 | /**
124 | * Casbin config object
125 | */
126 | export interface CasbinConfig {
127 | model: string | Model;
128 | policy?: string | PostgresAdapter | FileAdapter;
129 | allowedRes?: ResourcePermissionObject[];
130 | }
131 |
132 | export interface ResourcePermissionObject {
133 | resource: string;
134 | permission: string;
135 | }
136 |
137 | export interface IAuthUser {
138 | id?: number | string;
139 | username: string;
140 | password?: string;
141 | }
142 |
143 | export interface IUserPrefs {
144 | locale?: string;
145 | }
146 |
147 | export interface IAuthUserWithPermissions extends IAuthUser {
148 | id?: string;
149 | identifier?: ID;
150 | permissions: string[];
151 | authClientId: number;
152 | userPreferences?: IUserPrefs;
153 | email?: string;
154 | role: string;
155 | firstName: string;
156 | lastName: string;
157 | middleName?: string;
158 | allowedResources?: string[];
159 | }
160 | export interface IUserResource {
161 | resourceName: string;
162 | resourceValue: ResourceValueType;
163 | allowed: boolean;
164 | }
165 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/tsconfig",
3 | "extends": "@loopback/build/config/tsconfig.common.json",
4 | "compilerOptions": {
5 | "rootDir": "src",
6 | "outDir": "dist",
7 | "experimentalDecorators": true
8 | },
9 | "include": ["src", ".eslintrc.js"]
10 | }
11 |
--------------------------------------------------------------------------------