diff --git a/examples/access-control-migration/.dockerignore b/examples/access-control-migration/.dockerignore new file mode 100644 index 000000000000..7aecc7e3dda5 --- /dev/null +++ b/examples/access-control-migration/.dockerignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +/dist +# Cache used by TypeScript's incremental build +*.tsbuildinfo diff --git a/examples/access-control-migration/.eslintrc.js b/examples/access-control-migration/.eslintrc.js new file mode 100644 index 000000000000..96a619049d5c --- /dev/null +++ b/examples/access-control-migration/.eslintrc.js @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = { + extends: ['@loopback/eslint-config'], +}; diff --git a/examples/access-control-migration/.npmignore b/examples/access-control-migration/.npmignore new file mode 100644 index 000000000000..f86f365ab85b --- /dev/null +++ b/examples/access-control-migration/.npmignore @@ -0,0 +1,2 @@ +# Exclude *.tsbuildinfo - cache for tsc incremental builds +*.tsbuildinfo diff --git a/examples/access-control-migration/.npmrc b/examples/access-control-migration/.npmrc new file mode 100644 index 000000000000..cafe685a112d --- /dev/null +++ b/examples/access-control-migration/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/examples/access-control-migration/.prettierignore b/examples/access-control-migration/.prettierignore new file mode 100644 index 000000000000..c6911da9e1e8 --- /dev/null +++ b/examples/access-control-migration/.prettierignore @@ -0,0 +1,2 @@ +dist +*.json diff --git a/examples/access-control-migration/.prettierrc b/examples/access-control-migration/.prettierrc new file mode 100644 index 000000000000..f58b81dd7be2 --- /dev/null +++ b/examples/access-control-migration/.prettierrc @@ -0,0 +1,6 @@ +{ + "bracketSpacing": false, + "singleQuote": true, + "printWidth": 80, + "trailingComma": "all" +} diff --git a/examples/access-control-migration/.vscode/launch.json b/examples/access-control-migration/.vscode/launch.json new file mode 100644 index 000000000000..d90497e32056 --- /dev/null +++ b/examples/access-control-migration/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/index.js" + } + ] +} \ No newline at end of file diff --git a/examples/access-control-migration/.vscode/settings.json b/examples/access-control-migration/.vscode/settings.json new file mode 100644 index 000000000000..7c80afccc2b4 --- /dev/null +++ b/examples/access-control-migration/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "editor.rulers": [80], + "editor.tabCompletion": "on", + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "editor.formatOnSave": true, + + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.hg": true, + "**/.svn": true, + "**/CVS": true, + "dist": true, + }, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + + "typescript.tsdk": "./node_modules/typescript/lib" +} diff --git a/examples/access-control-migration/.vscode/tasks.json b/examples/access-control-migration/.vscode/tasks.json new file mode 100644 index 000000000000..555f092d81eb --- /dev/null +++ b/examples/access-control-migration/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Watch and Compile Project", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "build:watch"], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$tsc-watch" + }, + { + "label": "Build, Test and Lint", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "test:dev"], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": ["$tsc", "$eslint-stylish", "$eslint-compact"] + } + ] +} diff --git a/examples/access-control-migration/Dockerfile b/examples/access-control-migration/Dockerfile new file mode 100644 index 000000000000..bca90e201a53 --- /dev/null +++ b/examples/access-control-migration/Dockerfile @@ -0,0 +1,28 @@ +# Check out https://hub.docker.com/_/node to select a new base image +FROM node:10-slim + +# Set to a non-root built-in user `node` +USER node + +# Create app directory (with user `node`) +RUN mkdir -p /home/node/app + +WORKDIR /home/node/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY --chown=node package*.json ./ + +RUN npm install + +# Bundle app source code +COPY --chown=node . . + +RUN npm run build + +# Bind to all network interfaces so that it can be mapped to the host OS +ENV HOST=0.0.0.0 PORT=3000 + +EXPOSE ${PORT} +CMD [ "node", "." ] diff --git a/examples/access-control-migration/LICENSE b/examples/access-control-migration/LICENSE new file mode 100644 index 000000000000..05b33530dda0 --- /dev/null +++ b/examples/access-control-migration/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2020. All Rights Reserved. +Node module: @loopback/example-access-control-migration +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/examples/access-control-migration/README.md b/examples/access-control-migration/README.md new file mode 100644 index 000000000000..9f58a1808678 --- /dev/null +++ b/examples/access-control-migration/README.md @@ -0,0 +1,73 @@ +# @loopback/example-access-control-migration + +This example is migrated from +[loopback-example-access-control](https://github.com/strongloop/loopback-example-access-control), +and uses the authentication and authorization system in LoopBack 4 to implement +the access control. + +## Overview + +This tutorial demonstrates how to implement a RBAC(Role Based Access Control) +system and provides 5 endpoints to test different role's permissions. The +tutorial of building it from a dummy application documented in +![auth-example-migration-tutorial](https://github.com/strongloop/loopback-next/blob/auth-migration/docs/site/migration/auth/example.md) + +## Setup + +First, you'll need to install a supported version of Node: + +- [Node.js](https://nodejs.org/en/) at v10 or greater + +Additionally, this tutorial assumes that you are comfortable with certain +technologies, languages and concepts. + +- JavaScript (ES6) +- [REST](http://www.restapitutorial.com/lessons/whatisrest.html) + +Lastly, you'll need to install the LoopBack 4 CLI toolkit: + +```sh +npm i -g @loopback/cli +``` + +## Try it out + +If you'd like to see the final results of this tutorial as an example +application, follow these steps: + +```sh + $ npm start + + Server is running at http://127.0.0.1:3000 +``` + +Then try different roles' permissions by following the +[try it out section](https://github.com/strongloop/loopback-next/blob/auth-migration/docs/site/migration/auth/example.md#try-it-out) + +### Need help? + +Check out our [Gitter channel](https://gitter.im/strongloop/loopback) and ask +for help with this tutorial. + +### Bugs/Feedback + +Open an issue in [loopback-next](https://github.com/strongloop/loopback-next) +and we'll take a look. + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/examples/access-control-migration/data/db.json b/examples/access-control-migration/data/db.json new file mode 100644 index 000000000000..f692036839b0 --- /dev/null +++ b/examples/access-control-migration/data/db.json @@ -0,0 +1,28 @@ +{ + "ids": { + "Project": 3, + "Team": 3, + "User": 4, + "UserCredentials": 4 + }, + "models": { + "Project": { + "1": "{\"id\":1,\"name\":\"project1\",\"balance\":90,\"ownerId\":1}", + "2": "{\"id\":2,\"name\":\"project2\",\"balance\":0,\"ownerId\":2}" + }, + "Team": { + "1": "{\"id\":1,\"ownerId\":1,\"memberIds\":[1,2]}", + "2": "{\"id\":2,\"ownerId\":2,\"memberIds\":[2]}" + }, + "User": { + "1": "{\"id\":1,\"username\":\"John\",\"email\":\"john@doe.com\"}", + "2": "{\"id\":2,\"username\":\"Jane\",\"email\":\"jane@doe.com\"}", + "3": "{\"id\":3,\"username\":\"Bob\",\"email\":\"bob@projects.com\"}" + }, + "UserCredentials": { + "1": "{\"password\":\"$2a$10$Yn1/fMOjW6A.CdH7Yxb7weVFYmkcQJQBaYaiRScS6sw7ty3aL4lHu\",\"userId\":1,\"id\":1}", + "2": "{\"password\":\"$2a$10$Yn1/fMOjW6A.CdH7Yxb7weVFYmkcQJQBaYaiRScS6sw7ty3aL4lHu\",\"userId\":2,\"id\":2}", + "3": "{\"password\":\"$2a$10$Yn1/fMOjW6A.CdH7Yxb7weVFYmkcQJQBaYaiRScS6sw7ty3aL4lHu\",\"userId\":3,\"id\":3}" + } + } +} \ No newline at end of file diff --git a/examples/access-control-migration/fixtures/casbin/rbac_model.conf b/examples/access-control-migration/fixtures/casbin/rbac_model.conf new file mode 100644 index 000000000000..71159e387d34 --- /dev/null +++ b/examples/access-control-migration/fixtures/casbin/rbac_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/access-control-migration/fixtures/casbin/rbac_policy.admin.csv b/examples/access-control-migration/fixtures/casbin/rbac_policy.admin.csv new file mode 100644 index 000000000000..737e22e9dfe3 --- /dev/null +++ b/examples/access-control-migration/fixtures/casbin/rbac_policy.admin.csv @@ -0,0 +1,4 @@ +p, admin, project*, view-all +p, admin, project1, donate +p, admin, project2, donate +g, u3, admin \ No newline at end of file diff --git a/examples/access-control-migration/fixtures/casbin/rbac_policy.owner.csv b/examples/access-control-migration/fixtures/casbin/rbac_policy.owner.csv new file mode 100644 index 000000000000..cfbaa311a6ce --- /dev/null +++ b/examples/access-control-migration/fixtures/casbin/rbac_policy.owner.csv @@ -0,0 +1,8 @@ +p, p1_owner, project1, show-balance +p, p1_owner, project1, donate +p, p1_owner, project1, withdraw +g, u1, p1_owner +p, p2_owner, project2, show-balance +p, p2_owner, project2, donate +p, p2_owner, project2, withdraw +g, u2, p2_owner diff --git a/examples/access-control-migration/fixtures/casbin/rbac_policy.team_member.csv b/examples/access-control-migration/fixtures/casbin/rbac_policy.team_member.csv new file mode 100644 index 000000000000..49a1ed509b59 --- /dev/null +++ b/examples/access-control-migration/fixtures/casbin/rbac_policy.team_member.csv @@ -0,0 +1,7 @@ +p, p1_team, project1, show-balance +p, p1_team, project1, donate +g, u1, p1_team +g, u2, p1_team +p, p2_team, project2, show-balance +p, p2_team, project2, donate +g, u2, p2_team \ No newline at end of file diff --git a/examples/access-control-migration/index.d.ts b/examples/access-control-migration/index.d.ts new file mode 100644 index 000000000000..737cc5bf8047 --- /dev/null +++ b/examples/access-control-migration/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/examples/access-control-migration/index.js b/examples/access-control-migration/index.js new file mode 100644 index 000000000000..fa2916f2d130 --- /dev/null +++ b/examples/access-control-migration/index.js @@ -0,0 +1,26 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const application = require('./dist'); + +module.exports = application; + +if (require.main === module) { + // Run the application + const config = { + rest: { + port: +process.env.PORT || 3000, + host: process.env.HOST || 'localhost', + openApiSpec: { + // useful when used with OpenAPI-to-GraphQL to locate your application + setServersFromRequest: true, + }, + }, + }; + application.main(config).catch(err => { + console.error('Cannot start the application.', err); + process.exit(1); + }); +} diff --git a/examples/access-control-migration/index.ts b/examples/access-control-migration/index.ts new file mode 100644 index 000000000000..e8638d0c7c32 --- /dev/null +++ b/examples/access-control-migration/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/examples/access-control-migration/package-lock.json b/examples/access-control-migration/package-lock.json new file mode 100644 index 000000000000..30a3d811a551 --- /dev/null +++ b/examples/access-control-migration/package-lock.json @@ -0,0 +1,1795 @@ +{ + "name": "@loopback/example-access-control-migration", + "version": "1.9.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", + "dev": true + }, + "@types/node": { + "version": "10.17.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.15.tgz", + "integrity": "sha512-daFGV9GSs6USfPgxceDA8nlSe48XrVCJfDeYm7eokxq/ye7iuOH87hKXgMtEAVLFapkczbZsx868PMDT1Y0a6A==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.20.0.tgz", + "integrity": "sha512-cimIdVDV3MakiGJqMXw51Xci6oEDEoPkvh8ggJe2IIzcc0fYqAxOXN6Vbeanahz6dLZq64W+40iUEc9g32FLDQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.20.0", + "eslint-utils": "^1.4.3", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.20.0.tgz", + "integrity": "sha512-fEBy9xYrwG9hfBLFEwGW2lKwDRTmYzH3DwTmYbT+SMycmxAoPl0eGretnBFj/s+NfYBG63w/5c3lsvqqz5mYag==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.20.0", + "eslint-scope": "^5.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.20.0.tgz", + "integrity": "sha512-o8qsKaosLh2qhMZiHNtaHKTHyCHc3Triq6aMnwnWj7budm3xAY9owSZzV1uon5T9cWmJRJGzTFa90aex4m77Lw==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.20.0", + "@typescript-eslint/typescript-estree": "2.20.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.20.0.tgz", + "integrity": "sha512-WlFk8QtI8pPaE7JGQGxU7nGcnk1ccKAJkhbVookv94ZcAef3m6oCE/jEDL6dGte3JcD7reKrA0o55XhBRiVT3A==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "accept-language": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", + "integrity": "sha1-9QJfF79lpGaoRYOMz5jNuHfYM4Q=", + "requires": { + "bcp47": "^1.1.2", + "stable": "^0.1.6" + } + }, + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "dev": true + }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "dev": true + }, + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", + "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcp47": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", + "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "casbin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/casbin/-/casbin-3.1.0.tgz", + "integrity": "sha512-Ryax5OMnAuFYzTouADap6Vj6kPwlYxsWajxpzgR07tenn+O49mzouO2BMFqR9LrGvPX0GyWWklWURTvYuF4pfw==", + "requires": { + "expression-eval": "^2.0.0", + "ip": "^1.1.5", + "lodash": "^4.17.15" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "cldrjs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.1.tgz", + "integrity": "sha512-xyiP8uAm8K1IhmpDndZLraloW1yqu0L+HYdQ7O1aGPxx9Cr+BMnPANlNhSt++UKfxytL2hd2NPXgTjiy7k43Ew==" + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz", + "integrity": "sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-plugin-eslint-plugin": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.2.1.tgz", + "integrity": "sha512-nvmoefIqdFX+skyCt/dN9HaeSNyL8A9UvEtCqCFfJBjKpAR0uRL3SGPLlvDsnfXWtN72G/viowvpA33VjQkGCg==", + "dev": true + }, + "eslint-plugin-mocha": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.2.2.tgz", + "integrity": "sha512-oNhPzfkT6Q6CJ0HMVJ2KLxEWG97VWGTmuHOoRcDLE0U88ugUyFNV9wrT2XIt5cGtqc5W9k38m4xTN34L09KhBA==", + "dev": true, + "requires": { + "ramda": "^0.26.1" + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "dev": true, + "requires": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", + "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expression-eval": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/expression-eval/-/expression-eval-2.1.0.tgz", + "integrity": "sha512-FUJO/Akvl/JOWkvlqZaqbkhsEWlCJWDeZG4tzX96UH68D9FeRgYgtb55C2qtqbORC0Q6x5419EDjWu4IT9kQfg==", + "requires": { + "jsep": "^0.3.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globalize": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.4.2.tgz", + "integrity": "sha512-IfKeYI5mAITBmT5EnH8kSQB5uGson4Fkj2XtTpyEbIS7IHNfLHoeTyLJ6tfjiKC6cJXng3IhVurDk5C7ORqFhQ==", + "requires": { + "cldrjs": "^0.5.0" + } + }, + "globals": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", + "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", + "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsep": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.3.4.tgz", + "integrity": "sha512-ovGD9wE+wvudIIYxZGrRcZCxNyZ3Cl1N7Bzyp7/j4d/tA0BaUwcVM9bu0oZaSrefMiNwv6TwZ9X15gvZosteCQ==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonpath-plus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-1.1.0.tgz", + "integrity": "sha512-ydqTBOuLcFCUr9e7AxJlKCFgxzEQ03HjnIim0hJSdk2NxD8MOsaMOrRgP6XWEm5q3VuDY5+cRT1DM9vLlGo/qA==" + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "loopback-connector-rest": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/loopback-connector-rest/-/loopback-connector-rest-3.6.0.tgz", + "integrity": "sha512-SBMy66Lloh2Y7WLToITNRed8DeN710TP0DHXYCdXVb4ds0KmkanaTCy3y9urw0/t1PCk2lTf1aJbFKH5vg0+WQ==", + "requires": { + "debug": "^4.1.0", + "jsonpath-plus": "^1.1.0", + "lodash": "^4.17.11", + "methods": "^1.1.1", + "mime": "^2.3.1", + "qs": "^6.1.0", + "request": "^2.53.0", + "strong-globalize": "^4.1.1", + "traverse": "^0.6.6" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", + "requires": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz", + "integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==" + }, + "ramda": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", + "dev": true + }, + "regexpp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", + "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", + "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "strong-globalize": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-4.1.3.tgz", + "integrity": "sha512-SJegV7w5D4AodEspZJtJ7rls3fmi+Zc0PdyJCqBsg4RN9B8TC80/uAI2fikC+s1Jp9FLvr2vDX8f0Fqc62M4OA==", + "requires": { + "accept-language": "^3.0.18", + "debug": "^4.1.1", + "globalize": "^1.4.2", + "lodash": "^4.17.4", + "md5": "^2.2.1", + "mkdirp": "^0.5.1", + "os-locale": "^3.1.0", + "yamljs": "^0.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typescript": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + } + } + } +} diff --git a/examples/access-control-migration/package.json b/examples/access-control-migration/package.json new file mode 100644 index 000000000000..e44ed88d3a7a --- /dev/null +++ b/examples/access-control-migration/package.json @@ -0,0 +1,81 @@ +{ + "name": "@loopback/example-access-control-migration", + "version": "1.0.0", + "description": "Tutorial example on how to migrate the access control example with LoopBack 4.", + "main": "index.js", + "engines": { + "node": ">=10" + }, + "author": "IBM Corp.", + "scripts": { + "build": "lb-tsc", + "build:watch": "lb-tsc --watch", + "clean": "lb-clean *example-access-control-migration*.tgz dist *.tsbuildinfo package", + "lint": "npm run prettier:check && npm run eslint", + "lint:fix": "npm run eslint:fix && npm run prettier:fix", + "prettier:cli": "lb-prettier \"**/*.ts\"", + "prettier:check": "npm run prettier:cli -- -l", + "prettier:fix": "npm run prettier:cli -- --write", + "eslint": "lb-eslint --report-unused-disable-directives .", + "eslint:fix": "npm run eslint -- --fix", + "pretest": "npm run build", + "test": "lb-mocha \"dist/__tests__/**/*.js\"", + "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js", + "verify": "npm pack && tar xf loopback-access-control-migration*.tgz && tree package && npm run clean", + "migrate": "node ./dist/migrate", + "prestart": "npm run build", + "start": "node ." + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git", + "directory": "examples/access-control-migration" + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "dependencies": { + "@loopback/authentication": "^3.3.3", + "@loopback/authorization": "^0.4.10", + "@loopback/boot": "^1.7.4", + "@loopback/context": "^2.1.1", + "@loopback/core": "^1.12.4", + "@loopback/openapi-v3": "^2.0.0", + "@loopback/repository": "^1.19.1", + "@loopback/rest": "^2.0.0", + "@loopback/rest-explorer": "^1.4.10", + "@loopback/security": "^0.1.13", + "@loopback/service-proxy": "^1.3.17", + "bcryptjs": "^2.4.3", + "@types/bcryptjs": "2.4.2", + "casbin": "^3.1.0", + "jsonwebtoken": "^8.5.1", + "loopback-connector-rest": "^3.6.0" + }, + "devDependencies": { + "@loopback/build": "^3.1.1", + "@loopback/eslint-config": "^5.0.3", + "@loopback/http-caching-proxy": "^1.3.0", + "@loopback/testlab": "^1.10.3", + "@types/lodash": "^4.14.149", + "@types/node": "^10.17.14", + "@typescript-eslint/eslint-plugin": "^2.19.0", + "@typescript-eslint/parser": "^2.19.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-eslint-plugin": "^2.2.1", + "eslint-plugin-mocha": "^6.2.2", + "lodash": "^4.17.15", + "typescript": "~3.7.5" + }, + "keywords": [ + "loopback", + "LoopBack", + "example", + "tutorial", + "CRUD", + "models", + "todo" + ] +} diff --git a/examples/access-control-migration/public/index.html b/examples/access-control-migration/public/index.html new file mode 100644 index 000000000000..5a92bba48752 --- /dev/null +++ b/examples/access-control-migration/public/index.html @@ -0,0 +1,72 @@ + + + + + @loopback/example-access-control-migration + + + + + + + + + + +
+

@loopback/example-access-control-migration

+ +

OpenAPI spec: /openapi.json

+

API Explorer: /explorer

+
+ + + + + diff --git a/examples/access-control-migration/src/__tests__/acceptance/project.acceptance.ts b/examples/access-control-migration/src/__tests__/acceptance/project.acceptance.ts new file mode 100644 index 000000000000..d5433dc581d3 --- /dev/null +++ b/examples/access-control-migration/src/__tests__/acceptance/project.acceptance.ts @@ -0,0 +1,197 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {AnyObject} from '@loopback/repository'; +import { + Client, + createRestAppClient, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {AccessControlApplication} from '../../'; + +describe('AccessControlApplication - permissions', () => { + let app: AccessControlApplication; + let client: Client; + let token: string; + + before(givenRunningApplication); + before(() => { + client = createRestAppClient(app); + }); + after(async () => { + process.env.SEED_DATA = undefined; + await app.stop(); + }); + + const USER_CREDENTIAL_MAPPING: AnyObject = { + admin: ['bob@projects.com', 'opensesame'], + owner: ['john@doe.com', 'opensesame'], + team: ['jane@doe.com', 'opensesame'], + }; + + context('admin', () => { + const permissions = { + list: true, + view: true, + balance: false, + donate: true, + withdraw: false, + }; + testPermission('admin', permissions); + }); + + context('owner', () => { + const permissions = { + list: true, + view: false, + balance: true, + donate: true, + withdraw: true, + }; + testPermission('owner', permissions); + }); + + context('team-member', () => { + const permissions = { + list: true, + view: false, + balance: true, + donate: true, + withdraw: false, + }; + testPermission('team', permissions); + }); + + context('anonymous', () => { + const permissions = { + list: true, + view: false, + balance: false, + donate: false, + withdraw: false, + }; + testPermission('anonymous', permissions); + }); + + /** + * Test a role's permission when visit the 5 endpoints in the + * project controller + * @param role + * @param permissions + */ + function testPermission( + role: string, + permissions: {[operation: string]: boolean}, + ) { + it(`role ${role} login successfully`, async () => { + const credentials = USER_CREDENTIAL_MAPPING[role]; + // for anonymous user + if (!credentials) return; + + const res = await client + .post('/users/login') + .send({email: credentials[0], password: credentials[1]}) + .expect(200); + + token = res.body.token; + }); + + it(`list-projects returns ${permissions.list} for role ${role}`, async () => { + if (role === 'anonymous') { + await client.get('/list-projects').expect(200); + return; + } + + await client + .get('/list-projects') + .set('Authorization', 'Bearer ' + token) + .expect(200); + }); + + it(`view returns ${permissions.view} for role ${role}`, async () => { + const expectedStatus = permissions.view ? 200 : 401; + + if (role === 'anonymous') { + await client.get('/view-all-projects').expect(expectedStatus); + return; + } + + await client + .get('/view-all-projects') + .set('Authorization', 'Bearer ' + token) + .expect(expectedStatus); + }); + + it(`show-balance returns ${permissions.balance} for role ${role}`, async () => { + const expectedStatus = permissions.balance ? 200 : 401; + + if (role === 'anonymous') { + await client.get('/projects/1/show-balance').expect(expectedStatus); + return; + } + + await client + .get('/projects/1/show-balance') + .set('Authorization', 'Bearer ' + token) + .expect(expectedStatus); + }); + + it(`donate returns ${permissions.donate} for role ${role}`, async () => { + const expectedStatus = permissions.donate ? 204 : 401; + + if (role === 'anonymous') { + await client.patch('/projects/1/donate').expect(expectedStatus); + return; + } + + await client + .patch('/projects/1/donate') + .set('Authorization', 'Bearer ' + token) + .expect(expectedStatus); + }); + + it(`withdraw returns ${permissions.withdraw} for role ${role}`, async () => { + const expectedStatus = permissions.withdraw ? 204 : 401; + + if (role === 'anonymous') { + await client.patch('/projects/1/withdraw').expect(expectedStatus); + return; + } + + await client + .patch('/projects/1/withdraw') + .set('Authorization', 'Bearer ' + token) + .expect(expectedStatus); + }); + } + + /* + ============================================================================ + TEST HELPERS + These functions help simplify setup of your test fixtures so that your tests + can: + - operate on a "clean" environment each time (a fresh in-memory database) + - avoid polluting the test with large quantities of setup logic to keep + them clear and easy to read + - keep them DRY (who wants to write the same stuff over and over?) + ============================================================================ + */ + + async function givenRunningApplication() { + process.env.SEED_DATA = '1'; + app = new AccessControlApplication({ + rest: givenHttpServerConfig(), + }); + + app.bind('datasources.config.db').to({ + name: 'db', + connector: 'memory', + }); + + await app.boot(); + // Start Application + await app.start(); + } +}); diff --git a/examples/access-control-migration/src/application.ts b/examples/access-control-migration/src/application.ts new file mode 100644 index 000000000000..1f8d71fa2a81 --- /dev/null +++ b/examples/access-control-migration/src/application.ts @@ -0,0 +1,114 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + AuthenticationComponent, + registerAuthenticationStrategy, +} from '@loopback/authentication'; +import { + AuthorizationComponent, + AuthorizationTags, +} from '@loopback/authorization'; +import {BootMixin} from '@loopback/boot'; +import {ApplicationConfig, BindingKey} from '@loopback/core'; +import {RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; +import {RestExplorerComponent} from '@loopback/rest-explorer'; +import {ServiceMixin} from '@loopback/service-proxy'; +import path from 'path'; +import { + TokenServiceBindings, + TokenServiceConstants, + UserServiceBindings, +} from './keys'; +import {MySequence} from './sequence'; +import {CasbinAuthorizationProvider} from './services/casbin.authorizer'; +import {getCasbinEnforcerByName} from './services/casbin.enforcers'; +import {JWTAuthenticationStrategy} from './services/jwt.auth.strategy'; +import {JWTService} from './services/jwt.service'; +import {SECURITY_SCHEME_SPEC} from './services/security.spec'; +import {MyUserService} from './services/user.service'; + +/** + * Information from package.json + */ +export interface PackageInfo { + name: string; + version: string; + description: string; +} +export const PackageKey = BindingKey.create('application.package'); + +export class AccessControlApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options: ApplicationConfig = {}) { + super(options); + + // Set up the custom sequence + this.sequence(MySequence); + + // Set up default home page + this.static('/', path.join(__dirname, '../public')); + + this.setUpBindings(); + this.addSecuritySpec(); + + this.component(RestExplorerComponent); + // Bind authentication component related elements + this.component(AuthenticationComponent); + this.component(AuthorizationComponent); + + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = { + controllers: { + // Customize ControllerBooter Conventions here + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, + }, + }; + } + + setUpBindings(): void { + this.bind(TokenServiceBindings.TOKEN_SECRET).to( + TokenServiceConstants.TOKEN_SECRET_VALUE, + ); + this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to( + TokenServiceConstants.TOKEN_EXPIRES_IN_VALUE, + ); + this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService); + + this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService); + + // authorization + this.bind('casbin.enforcer.factory').to(getCasbinEnforcerByName); + this.bind('authorizationProviders.casbin-provider') + .toProvider(CasbinAuthorizationProvider) + .tag(AuthorizationTags.AUTHORIZER); + + // authentication + registerAuthenticationStrategy(this, JWTAuthenticationStrategy); + } + + addSecuritySpec(): void { + this.api({ + openapi: '3.0.0', + info: { + title: 'access-control-example', + version: require('.././package.json').version, + }, + paths: {}, + components: {securitySchemes: SECURITY_SCHEME_SPEC}, + security: [ + { + jwt: [], + }, + ], + servers: [{url: '/'}], + }); + } +} diff --git a/examples/access-control-migration/src/controllers/index.ts b/examples/access-control-migration/src/controllers/index.ts new file mode 100644 index 000000000000..138651cb3e56 --- /dev/null +++ b/examples/access-control-migration/src/controllers/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './project.controller'; +export * from './user.controller'; diff --git a/examples/access-control-migration/src/controllers/project.controller.ts b/examples/access-control-migration/src/controllers/project.controller.ts new file mode 100644 index 000000000000..1e677eae280f --- /dev/null +++ b/examples/access-control-migration/src/controllers/project.controller.ts @@ -0,0 +1,159 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import _ from 'lodash'; +import {repository} from '@loopback/repository'; +import {param, get, getModelSchemaRef, patch} from '@loopback/rest'; +import {Project} from '../models'; +import {ProjectRepository} from '../repositories'; +import {authenticate} from '@loopback/authentication'; +import {authorize} from '@loopback/authorization'; +import {assignProjectInstanceId} from '../services/assign-project-instance-id.voter'; + +// TBD: refactor the ACLs to a separate file +const RESOURCE_NAME = 'project'; +const ACL_PROJECT = { + 'view-all': { + resource: `${RESOURCE_NAME}*`, + scopes: ['view-all'], + allowedRoles: ['admin'], + }, + 'show-balance': { + resource: RESOURCE_NAME, + scopes: ['show-balance'], + allowedRoles: ['owner', 'team'], + voters: [assignProjectInstanceId], + }, + donate: { + resource: RESOURCE_NAME, + scopes: ['donate'], + allowedRoles: ['admin', 'owner', 'team'], + voters: [assignProjectInstanceId], + }, + withdraw: { + resource: RESOURCE_NAME, + scopes: ['withdraw'], + allowedRoles: ['owner'], + voters: [assignProjectInstanceId], + }, +}; + +// TODO: add other CRUD methods and corresponding ACL +export class ProjectController { + constructor( + @repository(ProjectRepository) + public projectRepository: ProjectRepository, + ) {} + + // LIST PROJECTS (balance is not public) + @get('/list-projects', { + responses: { + '200': { + description: 'List all the project model instances without balance', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(Project, { + title: 'ProjectPublic', + exclude: ['balance'], + }), + }, + }, + }, + }, + }, + }) + async listProjects(): Promise[]> { + const projects = await this.projectRepository.find(); + return projects.map(p => _.omit(p, 'balance')); + } + + // VIWE ALL PROJECTS (including balance) + @get('/view-all-projects', { + responses: { + '200': { + description: 'Array of all Project model instances including balance', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(Project), + }, + }, + }, + }, + }, + }) + @authenticate('jwt') + @authorize(ACL_PROJECT['view-all']) + async viewAll(): Promise { + return this.projectRepository.find(); + } + + // SHOW BALANCE: get project by id + @get('/projects/{id}/show-balance', { + responses: { + '200': { + description: 'show balance of a project', + content: { + 'application/json': { + schema: getModelSchemaRef(Project), + }, + }, + }, + }, + }) + @authenticate('jwt') + @authorize(ACL_PROJECT['show-balance']) + async findById(@param.path.number('id') id: number): Promise { + return this.projectRepository.findById(id); + } + + // DONATE BY ID + @patch('/projects/{id}/donate', { + responses: { + '204': { + description: 'Project donate success', + }, + }, + }) + @authenticate('jwt') + @authorize(ACL_PROJECT.donate) + async donateById( + @param.path.number('id') id: number, + @param.query.number('amount') amount: number, + ): Promise { + const project = await this.projectRepository.findById(id); + await this.projectRepository.updateById(id, { + balance: project.balance + amount, + }); + // TBD: return new balance + } + + // WITHDRAW BY ID + @patch('/projects/{id}/withdraw', { + responses: { + '204': { + description: 'Project withdraw success', + }, + }, + }) + @authenticate('jwt') + @authorize(ACL_PROJECT.withdraw) + async withdrawById( + @param.path.number('id') id: number, + @param.query.number('amount') amount: number, + ): Promise { + const project = await this.projectRepository.findById(id); + if (project.balance < amount) { + throw new Error('Balance is not enough.'); + } + await this.projectRepository.updateById(id, { + balance: project.balance - amount, + }); + // TBD: return new balance + } +} diff --git a/examples/access-control-migration/src/controllers/user.controller.ts b/examples/access-control-migration/src/controllers/user.controller.ts new file mode 100644 index 000000000000..2d30d878c9d5 --- /dev/null +++ b/examples/access-control-migration/src/controllers/user.controller.ts @@ -0,0 +1,79 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// Uncomment these imports to begin using these cool features! + +import {inject} from '@loopback/context'; +import {post, requestBody} from '@loopback/rest'; +import {Credentials} from '../services/user.service'; +import {UserServiceBindings, TokenServiceBindings} from '../keys'; +import {UserService, TokenService} from '@loopback/authentication'; +import {User} from '../models'; + +const CredentialsSchema = { + type: 'object', + required: ['email', 'password'], + properties: { + email: { + type: 'string', + format: 'email', + }, + password: { + type: 'string', + minLength: 8, + }, + }, +}; + +export const CredentialsRequestBody = { + description: 'The input of login function', + required: true, + content: { + 'application/json': {schema: CredentialsSchema}, + }, +}; + +export class UserController { + constructor( + @inject(TokenServiceBindings.TOKEN_SERVICE) + public jwtService: TokenService, + @inject(UserServiceBindings.USER_SERVICE) + public userService: UserService, + ) {} + + @post('/users/login', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }) + async login( + @requestBody(CredentialsRequestBody) credentials: Credentials, + ): Promise<{token: string}> { + // ensure the user exists, and the password is correct + const user = await this.userService.verifyCredentials(credentials); + + // convert a User object into a UserProfile object (reduced set of properties) + const userProfile = this.userService.convertToUserProfile(user); + + // create a JSON Web Token based on the user profile + const token = await this.jwtService.generateToken(userProfile); + + return {token}; + } +} diff --git a/examples/access-control-migration/src/datasources/db.datasource.config.json b/examples/access-control-migration/src/datasources/db.datasource.config.json new file mode 100644 index 000000000000..a68f220be986 --- /dev/null +++ b/examples/access-control-migration/src/datasources/db.datasource.config.json @@ -0,0 +1,6 @@ +{ + "name": "db", + "connector": "memory", + "localStorage": "", + "file": "./data/db.json" +} diff --git a/examples/access-control-migration/src/datasources/db.datasource.ts b/examples/access-control-migration/src/datasources/db.datasource.ts new file mode 100644 index 000000000000..9556e316e852 --- /dev/null +++ b/examples/access-control-migration/src/datasources/db.datasource.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/core'; +import {juggler} from '@loopback/repository'; +import config from './db.datasource.config.json'; + +export class DbDataSource extends juggler.DataSource { + static dataSourceName = 'db'; + + constructor( + @inject('datasources.config.db', {optional: true}) + dsConfig: object = config, + ) { + super(dsConfig); + } +} diff --git a/examples/access-control-migration/src/datasources/index.ts b/examples/access-control-migration/src/datasources/index.ts new file mode 100644 index 000000000000..2ee03a3f0871 --- /dev/null +++ b/examples/access-control-migration/src/datasources/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './db.datasource'; diff --git a/examples/access-control-migration/src/index.ts b/examples/access-control-migration/src/index.ts new file mode 100644 index 000000000000..b17bafb04d4d --- /dev/null +++ b/examples/access-control-migration/src/index.ts @@ -0,0 +1,24 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {AccessControlApplication} from './application'; +import {ApplicationConfig} from '@loopback/core'; + +export async function main(options: ApplicationConfig = {}) { + const app = new AccessControlApplication(options); + await app.boot(); + await app.start(); + + const url = app.restServer.url; + console.log(`Server is running at ${url}`); + return app; +} + +// re-exports for our benchmark, not needed for the tutorial itself +export {AccessControlApplication}; + +export * from './models'; +export * from './repositories'; +export * from '@loopback/rest'; diff --git a/examples/access-control-migration/src/keys.ts b/examples/access-control-migration/src/keys.ts new file mode 100644 index 000000000000..7b5d33132547 --- /dev/null +++ b/examples/access-control-migration/src/keys.ts @@ -0,0 +1,34 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {TokenService, UserService} from '@loopback/authentication'; +import {BindingKey} from '@loopback/context'; +import {User} from './models'; +import {Credentials} from './services/user.service'; + +export namespace TokenServiceConstants { + export const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; + export const TOKEN_EXPIRES_IN_VALUE = '21600'; +} + +export namespace TokenServiceBindings { + export const TOKEN_SECRET = BindingKey.create( + 'authentication.jwt.secret', + ); + export const TOKEN_EXPIRES_IN = BindingKey.create( + 'authentication.jwt.expires.in.seconds', + ); + export const TOKEN_SERVICE = BindingKey.create( + 'services.authentication.jwt.tokenservice', + ); +} + +export namespace UserServiceBindings { + export const USER_SERVICE = BindingKey.create>( + 'services.user.service', + ); +} + +export const RESOURCE_ID = BindingKey.create('resourceId'); diff --git a/examples/access-control-migration/src/models/index.ts b/examples/access-control-migration/src/models/index.ts new file mode 100644 index 000000000000..3de593999985 --- /dev/null +++ b/examples/access-control-migration/src/models/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './user.model'; +export * from './team.model'; +export * from './project.model'; +export * from './user-credentials.model'; diff --git a/examples/access-control-migration/src/models/project.model.ts b/examples/access-control-migration/src/models/project.model.ts new file mode 100644 index 000000000000..5540d296938a --- /dev/null +++ b/examples/access-control-migration/src/models/project.model.ts @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property, belongsTo} from '@loopback/repository'; +import {User} from './user.model'; + +@model({settings: {strict: false}}) +export class Project extends Entity { + @property({ + type: 'number', + id: true, + generated: false, + }) + id: number; + + @property({ + type: 'string', + required: true, + }) + name: string; + + @property({ + type: 'number', + required: true, + }) + balance: number; + + @belongsTo(() => User) + ownerId: number; + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface ProjectRelations { + // describe navigational properties here +} + +export type ProjectWithRelations = Project & ProjectRelations; diff --git a/examples/access-control-migration/src/models/team.model.ts b/examples/access-control-migration/src/models/team.model.ts new file mode 100644 index 000000000000..f78f9070bd07 --- /dev/null +++ b/examples/access-control-migration/src/models/team.model.ts @@ -0,0 +1,48 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property} from '@loopback/repository'; + +@model({settings: {strict: false}}) +export class Team extends Entity { + @property({ + type: 'number', + id: true, + generated: false, + }) + id: number; + + @property({ + type: 'number', + required: true, + }) + ownerId: number; + + // REFACTOR + // The members should be specified by relation + // instead of using an array of Ids + @property({ + type: 'array', + itemType: 'number', + required: true, + }) + memberIds: number[]; + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface TeamRelations { + // describe navigational properties here +} + +export type TeamWithRelations = Team & TeamRelations; diff --git a/examples/access-control-migration/src/models/user-credentials.model.ts b/examples/access-control-migration/src/models/user-credentials.model.ts new file mode 100644 index 000000000000..58eef1952273 --- /dev/null +++ b/examples/access-control-migration/src/models/user-credentials.model.ts @@ -0,0 +1,44 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property} from '@loopback/repository'; + +@model({settings: {strict: false}}) +export class UserCredentials extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @property({ + type: 'string', + required: true, + }) + password: string; + + @property({ + type: 'number', + required: true, + }) + userId: number; + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface UserCredentialsRelations { + // describe navigational properties here +} + +export type UserCredentialsWithRelations = UserCredentials & + UserCredentialsRelations; diff --git a/examples/access-control-migration/src/models/user.model.ts b/examples/access-control-migration/src/models/user.model.ts new file mode 100644 index 000000000000..d19c076401d9 --- /dev/null +++ b/examples/access-control-migration/src/models/user.model.ts @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property, hasMany, hasOne} from '@loopback/repository'; +import {Team} from './team.model'; +import {UserCredentials} from './user-credentials.model'; + +@model({settings: {strict: false}}) +export class User extends Entity { + @property({ + type: 'number', + id: true, + generated: false, + }) + id: number; + + @property({ + type: 'string', + required: true, + }) + username: string; + + @property({ + type: 'string', + required: true, + }) + email: string; + + @hasMany(() => Team, {keyTo: 'ownerId'}) + teams: Team[]; + + @hasOne(() => UserCredentials) + userCredentials: UserCredentials; + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface UserRelations { + // describe navigational properties here +} + +export type UserWithRelations = User & UserRelations; diff --git a/examples/access-control-migration/src/observers/index.ts b/examples/access-control-migration/src/observers/index.ts new file mode 100644 index 000000000000..80f8e95fd844 --- /dev/null +++ b/examples/access-control-migration/src/observers/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './sample.observer'; diff --git a/examples/access-control-migration/src/observers/sample.observer.ts b/examples/access-control-migration/src/observers/sample.observer.ts new file mode 100644 index 000000000000..44345005a04e --- /dev/null +++ b/examples/access-control-migration/src/observers/sample.observer.ts @@ -0,0 +1,109 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Application, + CoreBindings, + inject, + lifeCycleObserver, // The decorator + LifeCycleObserver, +} from '@loopback/core'; +import * as _ from 'lodash'; +import {ProjectRepository} from '../repositories/project.repository'; +import {TeamRepository} from '../repositories/team.repository'; +import {UserRepository} from '../repositories/user.repository'; +import {genSalt, hash} from 'bcryptjs'; + +/** + * This class will be bound to the application as a `LifeCycleObserver` during + * `boot` + */ +@lifeCycleObserver('') +export class SampleObserver implements LifeCycleObserver { + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) private app: Application, + @inject('repositories.ProjectRepository') + private projectRepo: ProjectRepository, + @inject('repositories.TeamRepository') private teamRepo: TeamRepository, + @inject('repositories.UserRepository') private userRepo: UserRepository, + ) {} + + /** + * This method will be invoked when the application starts + */ + async start(): Promise { + // Add your logic for start + if (process.env.SEED_DATA) { + await this.createUsers(); + await this.createProjects(); + await this.createTeams(); + } + } + + /** + * This method will be invoked when the application stops + */ + async stop(): Promise { + // Add your logic for stop + } + + async createUsers(): Promise { + const hashedPassword = await this.hashPassword('opensesame', 10); + const users = [ + { + id: 1, + username: 'John', + email: 'john@doe.com', + password: hashedPassword, + }, + { + id: 2, + username: 'Jane', + email: 'jane@doe.com', + password: hashedPassword, + }, + { + id: 3, + username: 'Bob', + email: 'bob@projects.com', + password: hashedPassword, + }, + ]; + + for (const u of users) { + await this.userRepo.create(_.pick(u, ['id', 'email', 'username'])); + await this.userRepo + .userCredentials(u.id) + .create({password: u.password, userId: u.id}); + } + } + + async createProjects(): Promise { + const projects = [ + {id: 1, name: 'project1', balance: 0, ownerId: 1}, + {id: 2, name: 'project2', balance: 0, ownerId: 2}, + ]; + + for (const p of projects) { + await this.projectRepo.create(p); + } + } + + async createTeams(): Promise { + const teams = [ + {id: 1, ownerId: 1, memberIds: [1, 2]}, + {id: 2, ownerId: 2, memberIds: [2]}, + ]; + + for (const t of teams) { + await this.teamRepo.create(t); + } + } + + async hashPassword(password: string, rounds: number): Promise { + const salt = await genSalt(rounds); + return hash(password, salt); + } +} diff --git a/examples/access-control-migration/src/repositories/index.ts b/examples/access-control-migration/src/repositories/index.ts new file mode 100644 index 000000000000..be81a04a0068 --- /dev/null +++ b/examples/access-control-migration/src/repositories/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './team.repository'; +export * from './user.repository'; +export * from './project.repository'; +export * from './user-credentials.repository'; diff --git a/examples/access-control-migration/src/repositories/project.repository.ts b/examples/access-control-migration/src/repositories/project.repository.ts new file mode 100644 index 000000000000..ad80ec6f15ca --- /dev/null +++ b/examples/access-control-migration/src/repositories/project.repository.ts @@ -0,0 +1,32 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + DefaultCrudRepository, + repository, + BelongsToAccessor, +} from '@loopback/repository'; +import {Project, ProjectRelations, User} from '../models'; +import {DbDataSource} from '../datasources'; +import {inject, Getter} from '@loopback/core'; +import {UserRepository} from './user.repository'; + +export class ProjectRepository extends DefaultCrudRepository< + Project, + typeof Project.prototype.id, + ProjectRelations +> { + public readonly user: BelongsToAccessor; + + constructor( + @inject('datasources.db') dataSource: DbDataSource, + @repository.getter('UserRepository') + protected userRepositoryGetter: Getter, + ) { + super(Project, dataSource); + this.user = this.createBelongsToAccessorFor('owner', userRepositoryGetter); + this.registerInclusionResolver('user', this.user.inclusionResolver); + } +} diff --git a/examples/access-control-migration/src/repositories/team.repository.ts b/examples/access-control-migration/src/repositories/team.repository.ts new file mode 100644 index 000000000000..e1c7c686617c --- /dev/null +++ b/examples/access-control-migration/src/repositories/team.repository.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {DefaultCrudRepository} from '@loopback/repository'; +import {Team, TeamRelations} from '../models'; +import {DbDataSource} from '../datasources'; +import {inject} from '@loopback/core'; + +export class TeamRepository extends DefaultCrudRepository< + Team, + typeof Team.prototype.id, + TeamRelations +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(Team, dataSource); + } +} diff --git a/examples/access-control-migration/src/repositories/user-credentials.repository.ts b/examples/access-control-migration/src/repositories/user-credentials.repository.ts new file mode 100644 index 000000000000..6907183a403e --- /dev/null +++ b/examples/access-control-migration/src/repositories/user-credentials.repository.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {DefaultCrudRepository} from '@loopback/repository'; +import {UserCredentials, UserCredentialsRelations} from '../models'; +import {DbDataSource} from '../datasources'; +import {inject} from '@loopback/core'; + +export class UserCredentialsRepository extends DefaultCrudRepository< + UserCredentials, + typeof UserCredentials.prototype.id, + UserCredentialsRelations +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(UserCredentials, dataSource); + } +} diff --git a/examples/access-control-migration/src/repositories/user.repository.ts b/examples/access-control-migration/src/repositories/user.repository.ts new file mode 100644 index 000000000000..dad9dc35feb2 --- /dev/null +++ b/examples/access-control-migration/src/repositories/user.repository.ts @@ -0,0 +1,70 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + DefaultCrudRepository, + repository, + HasManyRepositoryFactory, + HasOneRepositoryFactory, +} from '@loopback/repository'; +import {User, UserRelations, Team, UserCredentials} from '../models'; +import {DbDataSource} from '../datasources'; +import {inject, Getter} from '@loopback/core'; +import {TeamRepository} from './team.repository'; +import {UserCredentialsRepository} from './user-credentials.repository'; + +export class UserRepository extends DefaultCrudRepository< + User, + typeof User.prototype.id, + UserRelations +> { + public readonly teams: HasManyRepositoryFactory< + Team, + typeof User.prototype.id + >; + + public readonly userCredentials: HasOneRepositoryFactory< + UserCredentials, + typeof User.prototype.id + >; + + constructor( + @inject('datasources.db') dataSource: DbDataSource, + @repository.getter('TeamRepository') + protected teamRepositoryGetter: Getter, + @repository.getter('UserCredentialsRepository') + protected userCredentialsRepositoryGetter: Getter< + UserCredentialsRepository + >, + ) { + super(User, dataSource); + this.userCredentials = this.createHasOneRepositoryFactoryFor( + 'userCredentials', + userCredentialsRepositoryGetter, + ); + this.registerInclusionResolver( + 'userCredentials', + this.userCredentials.inclusionResolver, + ); + this.teams = this.createHasManyRepositoryFactoryFor( + 'teams', + teamRepositoryGetter, + ); + this.registerInclusionResolver('teams', this.teams.inclusionResolver); + } + + async findCredentials( + userId: typeof User.prototype.id, + ): Promise { + try { + return await this.userCredentials(userId).get(); + } catch (err) { + if (err.code === 'ENTITY_NOT_FOUND') { + return undefined; + } + throw err; + } + } +} diff --git a/examples/access-control-migration/src/sequence.ts b/examples/access-control-migration/src/sequence.ts new file mode 100644 index 000000000000..4dbe29f00df5 --- /dev/null +++ b/examples/access-control-migration/src/sequence.ts @@ -0,0 +1,59 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Context, inject} from '@loopback/context'; +import { + FindRoute, + InvokeMethod, + ParseParams, + Reject, + RequestContext, + RestBindings, + Send, + SequenceHandler, +} from '@loopback/rest'; +import { + AuthenticationBindings, + AuthenticateFn, + AUTHENTICATION_STRATEGY_NOT_FOUND, + USER_PROFILE_NOT_FOUND, +} from '@loopback/authentication'; + +const SequenceActions = RestBindings.SequenceActions; + +export class MySequence implements SequenceHandler { + constructor( + @inject(RestBindings.Http.CONTEXT) public ctx: Context, + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) public send: Send, + @inject(SequenceActions.REJECT) public reject: Reject, + @inject(AuthenticationBindings.AUTH_ACTION) + protected authenticateRequest: AuthenticateFn, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + + //call authentication action + await this.authenticateRequest(request); + + const args = await this.parseParams(request, route); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (error) { + if ( + error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || + error.code === USER_PROFILE_NOT_FOUND + ) { + Object.assign(error, {statusCode: 401 /* Unauthorized */}); + } + this.reject(context, error); + } + } +} diff --git a/examples/access-control-migration/src/services/assign-project-instance-id.voter.ts b/examples/access-control-migration/src/services/assign-project-instance-id.voter.ts new file mode 100644 index 000000000000..b458eee48a7b --- /dev/null +++ b/examples/access-control-migration/src/services/assign-project-instance-id.voter.ts @@ -0,0 +1,48 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + AuthorizationContext, + AuthorizationDecision, + AuthorizationMetadata, +} from '@loopback/authorization'; +import {RESOURCE_ID} from '../keys'; + +/** + * Instance level authorizer for known endpoints + * - 'projects/{id}/show-balance' + * - 'projects/{id}/donate' + * - 'projects/{id}/withdraw' + * This function is used to modify the authorization context. + * It is not used for making a decision, so just returns ABSTAIN + * @param authorizationCtx + * @param metadata + */ +export async function assignProjectInstanceId( + authorizationCtx: AuthorizationContext, + metadata: AuthorizationMetadata, +) { + const projectId = authorizationCtx.invocationContext.args[0]; + const resourceId = getResourceName( + metadata.resource ?? authorizationCtx.resource, + projectId, + ); + // resourceId will override the resource name from metadata + authorizationCtx.invocationContext.bind(RESOURCE_ID).to(resourceId); + return AuthorizationDecision.ABSTAIN; +} + +/** + * Generate the resource name according to the naming convention + * in casbin policy + * @param resource resource name + * @param id resource instance's id + */ +function getResourceName(resource: string, id?: number): string { + // instance level name + if (id) return `${resource}${id}`; + // class level name + return `${resource}*`; +} diff --git a/examples/access-control-migration/src/services/casbin.authorizer.ts b/examples/access-control-migration/src/services/casbin.authorizer.ts new file mode 100644 index 000000000000..5bc31df97d63 --- /dev/null +++ b/examples/access-control-migration/src/services/casbin.authorizer.ts @@ -0,0 +1,87 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + AuthorizationContext, + AuthorizationDecision, + AuthorizationMetadata, + AuthorizationRequest, + Authorizer, +} from '@loopback/authorization'; +import {inject, Provider} from '@loopback/core'; +import * as casbin from 'casbin'; +import {RESOURCE_ID} from '../keys'; +const debug = require('debug')('loopback:example:acl'); +const DEFAULT_SCOPE = 'execute'; + +// Class level authorizer +export class CasbinAuthorizationProvider implements Provider { + constructor( + @inject('casbin.enforcer.factory') + private enforcerFactory: (name: string) => Promise, + ) {} + + /** + * @returns authenticateFn + */ + value(): Authorizer { + return this.authorize.bind(this); + } + + async authorize( + authorizationCtx: AuthorizationContext, + metadata: AuthorizationMetadata, + ): Promise { + const subject = this.getUserName(authorizationCtx.principals[0].id); + const resourceId = await authorizationCtx.invocationContext.get( + RESOURCE_ID, + {optional: true}, + ); + const object = resourceId ?? metadata.resource ?? authorizationCtx.resource; + + const request: AuthorizationRequest = { + subject, + object, + action: (metadata.scopes && metadata.scopes[0]) || DEFAULT_SCOPE, + }; + + const allowedRoles = metadata.allowedRoles; + + if (!allowedRoles) return AuthorizationDecision.ALLOW; + if (allowedRoles.length < 1) return AuthorizationDecision.DENY; + + let allow = false; + + // An optimization for ONLY searching among the allowed roles' policies + for (const role of allowedRoles) { + const enforcer = await this.enforcerFactory(role); + + const allowedByRole = await enforcer.enforce( + request.subject, + request.object, + request.action, + ); + + debug(`authorizer role: ${role}, result: ${allowedByRole}`); + if (allowedByRole) { + allow = true; + break; + } + } + + debug('final result: ', allow); + + if (allow) return AuthorizationDecision.ALLOW; + else if (allow === false) return AuthorizationDecision.DENY; + return AuthorizationDecision.ABSTAIN; + } + + // Generate the user name according to the naming convention + // in casbin policy + // A user's name would be `u${id}` + getUserName(id: number): string { + return `u${id}`; + } +} diff --git a/examples/access-control-migration/src/services/casbin.enforcers.ts b/examples/access-control-migration/src/services/casbin.enforcers.ts new file mode 100644 index 000000000000..75a2802faf4e --- /dev/null +++ b/examples/access-control-migration/src/services/casbin.enforcers.ts @@ -0,0 +1,34 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as casbin from 'casbin'; +import path from 'path'; + +const POLICY_PATHS = { + admin: '../../fixtures/casbin/rbac_policy.admin.csv', + owner: '../../fixtures/casbin/rbac_policy.owner.csv', + team: '../../fixtures/casbin/rbac_policy.team_member.csv', +}; + +export async function getCasbinEnforcerByName( + name: string, +): Promise { + const CASBIN_ENFORCERS: {[key: string]: Promise} = { + admin: createEnforcerByRole(POLICY_PATHS.admin), + owner: createEnforcerByRole(POLICY_PATHS.owner), + team: createEnforcerByRole(POLICY_PATHS.team), + }; + if (Object.prototype.hasOwnProperty.call(CASBIN_ENFORCERS, name)) + return CASBIN_ENFORCERS[name]; + return undefined; +} + +export async function createEnforcerByRole( + policyPath: string, +): Promise { + const conf = path.resolve(__dirname, '../../fixtures/casbin/rbac_model.conf'); + const policy = path.resolve(__dirname, policyPath); + return casbin.newEnforcer(conf, policy); +} diff --git a/examples/access-control-migration/src/services/index.ts b/examples/access-control-migration/src/services/index.ts new file mode 100644 index 000000000000..e13d9c3be264 --- /dev/null +++ b/examples/access-control-migration/src/services/index.ts @@ -0,0 +1,4 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT diff --git a/examples/access-control-migration/src/services/jwt.auth.strategy.ts b/examples/access-control-migration/src/services/jwt.auth.strategy.ts new file mode 100644 index 000000000000..60f9c519209a --- /dev/null +++ b/examples/access-control-migration/src/services/jwt.auth.strategy.ts @@ -0,0 +1,50 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/context'; +import {HttpErrors, Request} from '@loopback/rest'; +import {AuthenticationStrategy, TokenService} from '@loopback/authentication'; +import {UserProfile} from '@loopback/security'; +import {TokenServiceBindings} from '../keys'; + +export class JWTAuthenticationStrategy implements AuthenticationStrategy { + name = 'jwt'; + + constructor( + @inject(TokenServiceBindings.TOKEN_SERVICE) + public tokenService: TokenService, + ) {} + + async authenticate(request: Request): Promise { + const token: string = this.extractCredentials(request); + const userProfile: UserProfile = await this.tokenService.verifyToken(token); + return userProfile; + } + + extractCredentials(request: Request): string { + if (!request.headers.authorization) { + throw new HttpErrors.Unauthorized(`Authorization header not found.`); + } + + // for example : Bearer xxx.yyy.zzz + const authHeaderValue = request.headers.authorization; + + if (!authHeaderValue.startsWith('Bearer')) { + throw new HttpErrors.Unauthorized( + `Authorization header is not of type 'Bearer'.`, + ); + } + + //split the string into 2 parts : 'Bearer ' and the `xxx.yyy.zzz` + const parts = authHeaderValue.split(' '); + if (parts.length !== 2) + throw new HttpErrors.Unauthorized( + `Authorization header value has too many parts. It must follow the pattern: 'Bearer xx.yy.zz' where xx.yy.zz is a valid JWT token.`, + ); + const token = parts[1]; + + return token; + } +} diff --git a/examples/access-control-migration/src/services/jwt.service.ts b/examples/access-control-migration/src/services/jwt.service.ts new file mode 100644 index 000000000000..e36b83d4b275 --- /dev/null +++ b/examples/access-control-migration/src/services/jwt.service.ts @@ -0,0 +1,77 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/context'; +import {HttpErrors} from '@loopback/rest'; +import {promisify} from 'util'; +import {TokenService} from '@loopback/authentication'; +import {UserProfile, securityId} from '@loopback/security'; +import {TokenServiceBindings} from '../keys'; + +const jwt = require('jsonwebtoken'); +const signAsync = promisify(jwt.sign); +const verifyAsync = promisify(jwt.verify); + +export class JWTService implements TokenService { + constructor( + @inject(TokenServiceBindings.TOKEN_SECRET) + private jwtSecret: string, + @inject(TokenServiceBindings.TOKEN_EXPIRES_IN) + private jwtExpiresIn: string, + ) {} + + async verifyToken(token: string): Promise { + if (!token) { + throw new HttpErrors.Unauthorized( + `Error verifying token : 'token' is null`, + ); + } + + let userProfile: UserProfile; + + try { + // decode user profile from token + const decodedToken = await verifyAsync(token, this.jwtSecret); + // don't copy over token field 'iat' and 'exp', nor 'email' to user profile + userProfile = Object.assign( + {[securityId]: '', name: ''}, + { + [securityId]: decodedToken.id, + name: decodedToken.name, + id: decodedToken.id, + }, + ); + } catch (error) { + throw new HttpErrors.Unauthorized( + `Error verifying token : ${error.message}`, + ); + } + return userProfile; + } + + async generateToken(userProfile: UserProfile): Promise { + if (!userProfile) { + throw new HttpErrors.Unauthorized( + 'Error generating token : userProfile is null', + ); + } + const userInfoForToken = { + id: userProfile[securityId], + name: userProfile.name, + email: userProfile.email, + }; + // Generate a JSON Web Token + let token: string; + try { + token = await signAsync(userInfoForToken, this.jwtSecret, { + expiresIn: Number(this.jwtExpiresIn), + }); + } catch (error) { + throw new HttpErrors.Unauthorized(`Error encoding token : ${error}`); + } + + return token; + } +} diff --git a/examples/access-control-migration/src/services/security.spec.ts b/examples/access-control-migration/src/services/security.spec.ts new file mode 100644 index 000000000000..20a63d6d988e --- /dev/null +++ b/examples/access-control-migration/src/services/security.spec.ts @@ -0,0 +1,18 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {SecuritySchemeObject, ReferenceObject} from '@loopback/openapi-v3'; + +export const OPERATION_SECURITY_SPEC = [{jwt: []}]; +export type SecuritySchemeObjects = { + [securityScheme: string]: SecuritySchemeObject | ReferenceObject; +}; +export const SECURITY_SCHEME_SPEC: SecuritySchemeObjects = { + jwt: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, +}; diff --git a/examples/access-control-migration/src/services/user.service.ts b/examples/access-control-migration/src/services/user.service.ts new file mode 100644 index 000000000000..ee94e9aaedff --- /dev/null +++ b/examples/access-control-migration/src/services/user.service.ts @@ -0,0 +1,61 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-access-control-migration +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {UserService} from '@loopback/authentication'; +import {repository} from '@loopback/repository'; +import {HttpErrors} from '@loopback/rest'; +import {securityId, UserProfile} from '@loopback/security'; +import {compare} from 'bcryptjs'; +import {User} from '../models/user.model'; +import {UserRepository} from '../repositories/user.repository'; + +export type Credentials = { + email: string; + password: string; +}; + +export class MyUserService implements UserService { + constructor( + @repository(UserRepository) public userRepository: UserRepository, + ) {} + + async verifyCredentials(credentials: Credentials): Promise { + const invalidCredentialsError = 'Invalid email or password.'; + + const foundUser = await this.userRepository.findOne({ + where: {email: credentials.email}, + }); + if (!foundUser) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + const credentialsFound = await this.userRepository.findCredentials( + foundUser.id, + ); + if (!credentialsFound) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + const passwordMatched = await compare( + credentials.password, + credentialsFound.password, + ); + + if (!passwordMatched) { + throw new HttpErrors.Unauthorized(invalidCredentialsError); + } + + return foundUser; + } + + convertToUserProfile(user: User): UserProfile { + return { + [securityId]: user.id.toString(), + name: user.username, + id: user.id, + email: user.email, + }; + } +} diff --git a/examples/access-control-migration/tsconfig.build.json b/examples/access-control-migration/tsconfig.build.json new file mode 100644 index 000000000000..c7b8e49eaca5 --- /dev/null +++ b/examples/access-control-migration/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +}