├── .gitignore ├── .releases ├── 1.0.0.md ├── 2.0.0.md ├── 1.1.0.md ├── 2.0.1.md ├── 1.2.0.md └── 1.3.0.md ├── packages └── sdk-js │ ├── .npmrc │ ├── .npmignore │ ├── lib │ ├── security │ │ ├── .meta.js │ │ └── JwtProvider.js │ ├── common │ │ └── Configuration.js │ └── gateway │ │ ├── .meta.js │ │ └── EntitlementsGateway.js │ ├── package.json │ ├── README.md │ └── examples │ └── node │ └── example.js ├── .jshintrc ├── lerna.json ├── package.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /.releases/1.0.0.md: -------------------------------------------------------------------------------- 1 | **Initial Release** -------------------------------------------------------------------------------- /packages/sdk-js/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 9, 3 | "validthis": true, 4 | "-W018": true 5 | } 6 | -------------------------------------------------------------------------------- /.releases/2.0.0.md: -------------------------------------------------------------------------------- 1 | **Breaking changes** 2 | 3 | * Replaced `service` endpoint with `service/version` endpoint. 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "packages": [ 4 | "packages/**" 5 | ], 6 | "version": "2.0.1", 7 | "useWorkspaces": true 8 | } 9 | -------------------------------------------------------------------------------- /.releases/1.1.0.md: -------------------------------------------------------------------------------- 1 | **New Features** 2 | 3 | * Cache is not immediately populated on `EntitlementsGateway.connect` invocation unless `eager` parameter is supplied. -------------------------------------------------------------------------------- /packages/sdk-js/.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | npm-debug.log 5 | 6 | .releases 7 | examples 8 | test 9 | 10 | .gitignore 11 | .jshintrc 12 | gulpfile.js 13 | buildspec.yml 14 | -------------------------------------------------------------------------------- /.releases/2.0.1.md: -------------------------------------------------------------------------------- 1 | **Technical Enhancements** 2 | 3 | * Updated AWS CodeBuild integration to use Node.js version 20. 4 | * Updated [`Lerna`](https://lerna.js.org/) by three major versions. 5 | * Updated other dependencies implicitly. -------------------------------------------------------------------------------- /.releases/1.2.0.md: -------------------------------------------------------------------------------- 1 | **New Features** 2 | 3 | * Added `EntitlementsGateway.registerAuthorizationObserver` function (an alternative way to pass the observer). 4 | 5 | **Bug Fixes** 6 | 7 | * Added `eager` parameter to factory functions. -------------------------------------------------------------------------------- /.releases/1.3.0.md: -------------------------------------------------------------------------------- 1 | **Technical Enhancements** 2 | 3 | * Updated the repository to use Lerna. 4 | * Renamed the repository to entitlements-public. 5 | * Updated dependencies to use the next major version of [@barchart/common-js](https://github.com/barchart/common-js). -------------------------------------------------------------------------------- /packages/sdk-js/lib/security/.meta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A meta namespace containing signatures of anonymous functions. 3 | * 4 | * @namespace Callbacks 5 | */ 6 | 7 | /** 8 | * A function which returns a signed token. 9 | * 10 | * @public 11 | * @callback JwtTokenGenerator 12 | * @memberOf Callbacks 13 | * @returns {String|Promise} 14 | */ -------------------------------------------------------------------------------- /packages/sdk-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@barchart/entitlements-client-js", 3 | "version": "2.0.1", 4 | "description": "JavaScript SDK for the Barchart Entitlement Service", 5 | "author": { 6 | "name": "Bryan Ingle", 7 | "email": "bryan.ingle@barchart.com", 8 | "url": "https://www.barchart.com" 9 | }, 10 | "scripts": { 11 | "lint": "jshint ./ --exclude './node_modules/**'", 12 | "postversion": "lerna publish from-package" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+ssh://git@github.com/barchart/entitlements-public.git" 17 | }, 18 | "keywords": [ 19 | "Barchart", 20 | "JavaScript", 21 | "SDK" 22 | ], 23 | "dependencies": { 24 | "@barchart/common-js": "^4.2.0" 25 | }, 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@barchart/entitlements-public", 3 | "description": "Public packages for the Barchart Entitlements Service", 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "author": { 8 | "name": "Bryan Ingle", 9 | "email": "bryan.ingle@barchart.com", 10 | "url": "https://www.barchart.com" 11 | }, 12 | "private": true, 13 | "workspaces": [ 14 | "packages/*" 15 | ], 16 | "scripts": { 17 | "clean": "lerna clean --yes && rm -rf node_modules", 18 | "lint": "lerna run lint --stream", 19 | "test": "lerna run test --stream", 20 | "preversion": "git diff --exit-code", 21 | "release": "lerna version -m 'Release. Bump version number %v' --tag-version-prefix='' --force-publish --no-granular-pathspec" 22 | }, 23 | "devDependencies": { 24 | "jshint": "^2.12.0", 25 | "lerna": "^6.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @barchart/entitlements-public 2 | 3 | [![AWS CodeBuild](https://codebuild.us-east-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiblhOTmZ4bmtkUVJKb3ZvN2M0TUpzelZPaXp4R1ZXNFBXTGZzeDIvRDdBR2N3a0JDMzgzem1rOWpoVm0yVE5mVmpuSWRmSlU1L2lFSzZmbkJJWUM0eFkwPSIsIml2UGFyYW1ldGVyU3BlYyI6InZ5VUFZenpIMi9HVldhUEgiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master)](https://github.com/barchart/entitlements-private) 4 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) 5 | 6 | _Public_ packages for the Barchart Entitlements Service — a service for authorizing, tracking, and reporting user actions. 7 | 8 | ### Packages 9 | 10 | * [packages/sdk-js](./packages/sdk-js) - A JavaScript SDK to simplify interactions with the Barchart Entitlements Service. 11 | 12 | ### License 13 | 14 | This software is provided under the MIT license. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021, Barchart.com, Inc., http://www.barchart.com/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/barchart/barchart-common-js 6 | 7 | The following license applies to all parts of this software. 8 | 9 | ==== 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining 12 | a copy of this software and associated documentation files (the 13 | "Software"), to deal in the Software without restriction, including 14 | without limitation the rights to use, copy, modify, merge, publish, 15 | distribute, sublicense, and/or sell copies of the Software, and to 16 | permit persons to whom the Software is furnished to do so, subject to 17 | the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be 20 | included in all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 26 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 27 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 28 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/sdk-js/lib/common/Configuration.js: -------------------------------------------------------------------------------- 1 | module.exports = (() => { 2 | 'use strict'; 3 | 4 | /** 5 | * Static configuration data. 6 | * 7 | * @public 8 | * @ignore 9 | */ 10 | class Configuration { 11 | constructor() { 12 | 13 | } 14 | 15 | /** 16 | * The hostname of the REST API for the development environment (intended for Barchart use only). 17 | * 18 | * @public 19 | * @static 20 | * @returns {String} 21 | */ 22 | static get developmentHost() { 23 | return 'entitlements-dev.aws.barchart.com'; 24 | } 25 | 26 | /** 27 | * The hostname of the REST API for the staging environment (public use allowed). 28 | * 29 | * @public 30 | * @static 31 | * @returns {String} 32 | */ 33 | static get stagingHost() { 34 | return 'entitlements-stage.aws.barchart.com'; 35 | } 36 | 37 | /** 38 | * The hostname of the REST API for the production environment (public use allowed). 39 | * 40 | * @public 41 | * @static 42 | * @returns {String} 43 | */ 44 | static get productionHost() { 45 | return 'entitlements.aws.barchart.com'; 46 | } 47 | 48 | /** 49 | * The hostname of the REST API for the admin environment (intended for Barchart use only). 50 | * 51 | * @public 52 | * @static 53 | * @returns {String} 54 | */ 55 | static get adminHost() { 56 | return 'entitlements-admin.aws.barchart.com'; 57 | } 58 | 59 | /** 60 | * The hostname of REST API which generates impersonation tokens for non-secure 61 | * test and demo environments. 62 | * 63 | * @public 64 | * @static 65 | * @returns {string} 66 | */ 67 | static get getJwtImpersonationHost() { 68 | return 'jwt-public-prod.aws.barchart.com'; 69 | } 70 | 71 | toString() { 72 | return '[Configuration]'; 73 | } 74 | } 75 | 76 | return Configuration; 77 | })(); 78 | -------------------------------------------------------------------------------- /packages/sdk-js/README.md: -------------------------------------------------------------------------------- 1 | # @barchart/entitlements-client-js 2 | 3 | ### Overview 4 | 5 | This **JavaScript SDK** connects your application to the Barchart Entitlement Service. 6 | 7 | ### Integration 8 | 9 | #### Authentication 10 | 11 | Authentication is handled with [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token). Construct a [```JwtProvider```](packages/sdk-js/lib/security/JwtProvider) instance which generates tokens for the current user. 12 | 13 | #### Extensions 14 | 15 | You can supply an "observer" function which will be notified each time ```EntitlementsGateway.authorize``` is called. This could be used to trigger common UI components. Here is an example: 16 | 17 | ```js 18 | const myAuthorizationObserver = (request, response) => { 19 | console.log(JSON.stringify(request, null, 2)); 20 | console.log(JSON.stringify(response, null, 2)); 21 | }; 22 | ``` 23 | 24 | #### Setup 25 | 26 | Build an instance of the ```EntitlementsGateway``` as follows: 27 | 28 | ```js 29 | let myJwtProvider = JwtProvider.forDevelopment('00000000', 'BARCHART'); 30 | let myEntitlementsGateway; 31 | 32 | EntitlementsGateway.forDevelopment(myJwtProvider, myAuthorizationObserver) 33 | .then((gateway) => { 34 | myEntitlementsGateway = gateway; 35 | }); 36 | ``` 37 | 38 | #### Authorization 39 | 40 | Each time a restricted operation is attempted, invoke the ```EntitlementsGateway.authorize``` function. This will asynchronously return a ```Boolean``` value, indicating if the referenced operation should be permitted. 41 | 42 | ```js 43 | myEntitlementsGateway.authorize('some.operation') 44 | .then((authorized) => { 45 | if (authorized) { 46 | doSomeOperation(); 47 | } 48 | }); 49 | ``` 50 | 51 | ### Examples 52 | 53 | A simple Node.js script can be found in the `examples/node` directory. Review and execute as follows: 54 | 55 | ```shell 56 | > node ./examples/node/example.js 57 | ``` -------------------------------------------------------------------------------- /packages/sdk-js/examples/node/example.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | 3 | const EntitlementsGateway = require('../../lib/gateway/EntitlementsGateway'), 4 | EntitlementsJwtProvider = require('../../lib/security/JwtProvider'); 5 | 6 | const startup = (() => { 7 | 'use strict'; 8 | 9 | let entitlementsJwtProvider = null; 10 | let entitlementsGateway = null; 11 | 12 | process.on('SIGINT', () => { 13 | console.log('Example: Processing SIGINT'); 14 | 15 | if (entitlementsJwtProvider !== null) { 16 | entitlementsJwtProvider.dispose(); 17 | } 18 | 19 | if (entitlementsGateway !== null) { 20 | entitlementsGateway.dispose(); 21 | } 22 | 23 | console.log('Example: Node.js example script ending'); 24 | 25 | process.exit(); 26 | }); 27 | 28 | process.on('unhandledRejection', (error) => { 29 | console.error('Unhandled Promise Rejection', error); 30 | console.trace(); 31 | }); 32 | 33 | process.on('uncaughtException', (error) => { 34 | console.error('Unhandled Error', error); 35 | console.trace(); 36 | }); 37 | 38 | const user = '00000000'; 39 | const context = 'TGAM'; 40 | const permissions = 'globe-unlimited'; 41 | 42 | console.log(`Example: Configuring EntitlementsGateway to impersonate user [ ${user}/${context} ]`); 43 | 44 | const authorizationObserver = (request, response) => { 45 | console.log(`Example: Authorization observer notified.`); 46 | 47 | console.log(JSON.stringify(request, null, 2)); 48 | console.log(JSON.stringify(response, null, 2)); 49 | }; 50 | 51 | return EntitlementsGateway.forDevelopment(entitlementsJwtProvider = EntitlementsJwtProvider.forDevelopment(user, context, permissions), authorizationObserver) 52 | .then((gateway) => { 53 | entitlementsGateway = gateway; 54 | 55 | const operation = 'watchlist.exports.csv'; 56 | const data = null; 57 | 58 | console.log(`Example: Requesting authorization for operation [ ${operation} ]`); 59 | 60 | return entitlementsGateway.authorize(operation, data) 61 | .then((authorized) => { 62 | if (authorized) { 63 | console.log(`Example: Authorization granted for operation [ ${operation} ]`); 64 | } else { 65 | console.log(`Example: Authorization denied for operation [ ${operation} ]`); 66 | } 67 | }); 68 | }); 69 | })(); 70 | -------------------------------------------------------------------------------- /packages/sdk-js/lib/gateway/.meta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A meta namespace containing structural contracts of anonymous objects. 3 | * 4 | * @namespace Schema 5 | */ 6 | 7 | /** 8 | * An object containing information about the remote service. 9 | * 10 | * @typedef EntitlementServiceInfo 11 | * @type Object 12 | * @memberOf Schema 13 | * @property {Object} service 14 | * @property {String} service.name - The remote service's name. 15 | * @property {String} service.environment - The remote service's environment name (e.g. production, test, staging, etc). 16 | * @property {String} service.version - The remote service's software version. 17 | * @property {String} service.description - The remote service's description. 18 | */ 19 | 20 | /** 21 | * An object describing a user and his/her assigned roles. 22 | * 23 | * @typedef User 24 | * @type Object 25 | * @memberOf Schema 26 | * @property {String} user - The user's identifier. 27 | * @property {String} context - The user's context. 28 | * @property {Schema.Role[]} roles - The user's roles. 29 | */ 30 | 31 | /** 32 | * An object describing a role (which can be assigned to a user). 33 | * 34 | * @typedef Role 35 | * @type Object 36 | * @memberOf Schema 37 | * @property {String} role - The role's identifier. 38 | * @property {String} context - The roles's context. 39 | * @property {Schema.Permission[]} permissions 40 | */ 41 | 42 | /** 43 | * @typedef Permission 44 | * @type Object 45 | * @memberOf Schema 46 | * @property {Schema.Operation} operation 47 | */ 48 | 49 | /** 50 | * @typedef Operation 51 | * @type Object 52 | * @memberOf Schema 53 | * @property {String} operation 54 | * @property {String} product 55 | * @property {Schema.Restriction[]} restrictions 56 | */ 57 | 58 | /** 59 | * @typedef Restriction 60 | * @type Object 61 | * @memberOf Schema 62 | * @property {String} type 63 | */ 64 | 65 | 66 | /** 67 | * @typedef AuthorizationRequest 68 | * @type Object 69 | * @memberOf Schema 70 | * @property {Object} user 71 | * @property {String} user.user 72 | * @property {String} user.context 73 | * @property {String[]} user.roles 74 | * @property {Object} operation 75 | * @property {String} operation.operation 76 | * @property {String} operation.product 77 | * @property {String[]} operation.restrictions 78 | */ 79 | 80 | /** 81 | * @typedef AuthorizationResponse 82 | * @type Object 83 | * @memberOf Schema 84 | * @property {Boolean} authorized 85 | * @property {Schema.RestrictionAdvice[]} advice 86 | */ 87 | 88 | /** 89 | * @typedef RestrictionAdvice 90 | * @type Object 91 | * @memberOf Schema 92 | * @property {Object} restriction 93 | * @property {String} restriction.type 94 | * @property {Boolean} restricted 95 | * @property {Object} additional 96 | */ 97 | 98 | /** 99 | * A meta namespace containing signatures of anonymous functions. 100 | * 101 | * @namespace Callbacks 102 | */ 103 | 104 | /** 105 | * A function which observes the authorization of an operation. 106 | * 107 | * @public 108 | * @callback AuthorizationObserver 109 | * @memberOf Callbacks 110 | * @param {Schema.AuthorizationRequest} request 111 | * @param {Schema.AuthorizationResponse} response 112 | */ -------------------------------------------------------------------------------- /packages/sdk-js/lib/security/JwtProvider.js: -------------------------------------------------------------------------------- 1 | const assert = require('@barchart/common-js/lang/assert'), 2 | Disposable = require('@barchart/common-js/lang/Disposable'), 3 | is = require('@barchart/common-js/lang/is'), 4 | random = require('@barchart/common-js/lang/random'), 5 | Scheduler = require('@barchart/common-js/timing/Scheduler'); 6 | 7 | const FailureReason = require('@barchart/common-js/api/failures/FailureReason'), 8 | FailureType = require('@barchart/common-js/api/failures/FailureType'); 9 | 10 | const EndpointBuilder = require('@barchart/common-js/api/http/builders/EndpointBuilder'), 11 | Gateway = require('@barchart/common-js/api/http/Gateway'), 12 | ProtocolType = require('@barchart/common-js/api/http/definitions/ProtocolType'), 13 | ResponseInterceptor = require('@barchart/common-js/api/http/interceptors/ResponseInterceptor'), 14 | VerbType = require('@barchart/common-js/api/http/definitions/VerbType'); 15 | 16 | const Configuration = require('../common/Configuration'); 17 | 18 | module.exports = (() => { 19 | 'use strict'; 20 | 21 | const DEFAULT_REFRESH_INTERVAL_MILLISECONDS = 5 * 60 * 1000; 22 | 23 | /** 24 | * Generates and caches a signed token (using a delegate). The cached token 25 | * is refreshed periodically. 26 | * 27 | * @public 28 | * @exported 29 | * @param {Callbacks.JwtTokenGenerator} tokenGenerator - An anonymous function which returns a signed JWT token. 30 | * @param {Number=} refreshInterval - The number of milliseconds which must pass before a new JWT token is generated. A zero value means the token should never be refreshed. A null or undefined value means the token is not cached. 31 | * @param {Boolean=} anonymousSupport - If true, an anonymous user will be used (if the token generator fails) 32 | * @param {String=} anonymousContext - The context for an anonymous user. 33 | */ 34 | class JwtProvider extends Disposable { 35 | constructor(tokenGenerator, refreshInterval, anonymousSupport, anonymousContext) { 36 | super(); 37 | 38 | assert.argumentIsRequired(tokenGenerator, 'tokenGenerator', Function); 39 | assert.argumentIsOptional(refreshInterval, 'refreshInterval', Number); 40 | assert.argumentIsOptional(anonymousSupport, 'anonymousSupport', Boolean); 41 | assert.argumentIsOptional(anonymousContext, 'anonymousContext', String); 42 | 43 | this._tokenGenerator = tokenGenerator; 44 | 45 | this._tokenPromise = null; 46 | 47 | this._refreshTimestamp = null; 48 | this._refreshPending = false; 49 | 50 | if (is.number(refreshInterval)) { 51 | this._refreshInterval = Math.max(refreshInterval || 0, 0); 52 | this._refreshJitter = random.range(0, Math.floor(this._refreshInterval / 10)); 53 | } else { 54 | this._refreshInterval = null; 55 | this._refreshJitter = null; 56 | } 57 | 58 | this._anonymousSupport = anonymousSupport || false; 59 | this._anonymousContext = anonymousContext || null; 60 | 61 | this._scheduler = new Scheduler(); 62 | } 63 | 64 | /** 65 | * Reads the current token, refreshing if necessary. 66 | * 67 | * @public 68 | * @returns {Promise} 69 | */ 70 | getToken() { 71 | return Promise.resolve() 72 | .then(() => { 73 | if (this._refreshPending) { 74 | return this._tokenPromise; 75 | } 76 | 77 | if (this._tokenPromise === null || this._refreshInterval === null || (this._refreshInterval > 0 && getTime() > (this._refreshTimestamp + this._refreshInterval + this._refreshJitter))) { 78 | this._refreshPending = true; 79 | 80 | const generateToken = () => { 81 | return Promise.resolve() 82 | .then(() => { 83 | return this._tokenGenerator(); 84 | }).catch((e) => { 85 | let anonymous = false; 86 | 87 | if (this._anonymousSupport && e instanceof FailureReason && (e.hasFailureType(FailureType.REQUEST_IDENTITY_FAILURE) || e.hasFailureType(FailureType.REQUEST_AUTHORIZATION_FAILURE))) { 88 | anonymous = true; 89 | } 90 | 91 | if (anonymous && this._anonymousContext) { 92 | return Promise.resolve(`anonymous-${this._anonymousContext}`); 93 | } else { 94 | return Promise.reject(e); 95 | } 96 | }); 97 | }; 98 | 99 | this._tokenPromise = this._scheduler.backoff(generateToken, 100, 'Read JWT token', 3) 100 | .then((token) => { 101 | this._refreshTimestamp = getTime(); 102 | this._refreshPending = false; 103 | 104 | return token; 105 | }).catch((e) => { 106 | this._tokenPromise = null; 107 | 108 | this._refreshTimestamp = null; 109 | this._refreshPending = false; 110 | 111 | return Promise.reject(e); 112 | }); 113 | } 114 | 115 | return this._tokenPromise; 116 | }); 117 | } 118 | 119 | /** 120 | * A factory for {@link JwtProvider} which is an alternative to the constructor. 121 | * 122 | * @public 123 | * @static 124 | * @param {Callbacks.JwtTokenGenerator} tokenGenerator - An anonymous function which returns a signed JWT token. 125 | * @param {Number=} refreshInterval - The number of milliseconds which must pass before a new JWT token is generated. A zero value means the token should never be refreshed. A null or undefined value means the token is not cached. 126 | * @param {Boolean=} anonymousSupport 127 | * @param {String=} anonymousContext 128 | * @returns {JwtProvider} 129 | */ 130 | static fromTokenGenerator(tokenGenerator, refreshInterval, anonymousSupport, anonymousContext) { 131 | return new JwtProvider(tokenGenerator, refreshInterval, anonymousSupport, anonymousContext); 132 | } 133 | 134 | /** 135 | * Builds a {@link JwtProvider} which will generate tokens impersonating the specified 136 | * user. The "development" environment is for Barchart use only and access is restricted 137 | * to Barchart's internal network. 138 | * 139 | * @public 140 | * @static 141 | * @param {String} userId - The user identifier to impersonate. 142 | * @param {String} contextId - The context identifier of the user to impersonate. 143 | * @param {String=} permissions - The desired permission level. 144 | * @returns {JwtProvider} 145 | */ 146 | static forDevelopment(userId, contextId, permissions) { 147 | return getJwtProviderForImpersonation(Configuration.getJwtImpersonationHost, 'dev', userId, contextId, permissions); 148 | } 149 | 150 | /** 151 | * Builds a {@link JwtProvider} which will generate tokens impersonating the specified 152 | * user. The "admin" environment is for Barchart use only and access is restricted 153 | * to Barchart's internal network. 154 | * 155 | * @public 156 | * @static 157 | * @param {String} userId - The user identifier to impersonate. 158 | * @param {String} contextId - The context identifier of the user to impersonate. 159 | * @param {String=} permissions - The desired permission level. 160 | * @returns {JwtProvider} 161 | */ 162 | static forAdmin(userId, contextId, permissions) { 163 | return getJwtProviderForImpersonation(Configuration.getJwtImpersonationHost, 'admin', userId, contextId, permissions); 164 | } 165 | 166 | _onDispose() { 167 | this._scheduler.dispose(); 168 | this._scheduler = null; 169 | } 170 | 171 | toString() { 172 | return '[JwtProvider]'; 173 | } 174 | } 175 | 176 | function getJwtProviderForImpersonation(host, environment, userId, contextId, permissions) { 177 | assert.argumentIsRequired(host, 'host', String); 178 | assert.argumentIsRequired(environment, 'environment', String); 179 | assert.argumentIsRequired(userId, 'userId', String); 180 | assert.argumentIsRequired(contextId, 'contextId', String); 181 | assert.argumentIsOptional(permissions, 'permissions', String); 182 | 183 | const tokenEndpoint = EndpointBuilder.for('generate-impersonation-jwt-for-test', 'generate JWT token for test environment') 184 | .withVerb(VerbType.POST) 185 | .withProtocol(ProtocolType.HTTPS) 186 | .withHost(host) 187 | .withPathBuilder((pb) => 188 | pb.withLiteralParameter('version', 'v1') 189 | .withLiteralParameter('tokens', 'tokens') 190 | .withLiteralParameter('impersonate', 'impersonate') 191 | .withLiteralParameter('service', 'entitlements') 192 | .withLiteralParameter('environment', environment) 193 | ) 194 | .withBody() 195 | .withResponseInterceptor(ResponseInterceptor.DATA) 196 | .endpoint; 197 | 198 | const payload = { }; 199 | 200 | payload.userId = userId; 201 | payload.contextId = contextId; 202 | 203 | if (permissions) { 204 | payload.permissions = permissions; 205 | } 206 | 207 | return new JwtProvider(() => Gateway.invoke(tokenEndpoint, payload), DEFAULT_REFRESH_INTERVAL_MILLISECONDS); 208 | } 209 | 210 | function getTime() { 211 | return (new Date()).getTime(); 212 | } 213 | 214 | return JwtProvider; 215 | })(); 216 | -------------------------------------------------------------------------------- /packages/sdk-js/lib/gateway/EntitlementsGateway.js: -------------------------------------------------------------------------------- 1 | const assert = require('@barchart/common-js/lang/assert'), 2 | Disposable = require('@barchart/common-js/lang/Disposable'), 3 | Enum = require('@barchart/common-js/lang/Enum'), 4 | is = require('@barchart/common-js/lang/is'); 5 | 6 | const EndpointBuilder = require('@barchart/common-js/api/http/builders/EndpointBuilder'), 7 | Gateway = require('@barchart/common-js/api/http/Gateway'), 8 | ProtocolType = require('@barchart/common-js/api/http/definitions/ProtocolType'), 9 | ErrorInterceptor = require('@barchart/common-js/api/http/interceptors/ErrorInterceptor'), 10 | FailureReason = require('@barchart/common-js/api/failures/FailureReason'), 11 | FailureType = require('@barchart/common-js/api/failures/FailureType'), 12 | RequestInterceptor = require('@barchart/common-js/api/http/interceptors/RequestInterceptor'), 13 | ResponseInterceptor = require('@barchart/common-js/api/http/interceptors/ResponseInterceptor'), 14 | VerbType = require('@barchart/common-js/api/http/definitions/VerbType'); 15 | 16 | const Configuration = require('../common/Configuration'), 17 | JwtProvider = require('../security/JwtProvider'); 18 | 19 | module.exports = (() => { 20 | 'use strict'; 21 | 22 | const REST_API_SECURE_PROTOCOL = 'https'; 23 | const REST_API_SECURE_PORT = 443; 24 | 25 | const REFRESH_INTERVAL_MILLISECONDS = 10 * 60 * 1000; 26 | 27 | /** 28 | * The **central component of the SDK**. It is responsible for connecting to Barchart's 29 | * Entitlements Service. It can be used to query, edit, and delete entitlements. 30 | * 31 | * @public 32 | * @exported 33 | * @param {String} protocol - The protocol of the of the Entitlement web service (either http or https). 34 | * @param {String} host - The hostname of the Entitlements web service. 35 | * @param {Number} port - The TCP port number of the Entitlements web service. 36 | * @param {String} environment - A description of the environment we're connecting to. 37 | * @extends {Disposable} 38 | */ 39 | class EntitlementsGateway extends Disposable { 40 | constructor(protocol, host, port, environment) { 41 | super(); 42 | 43 | this._environment = environment; 44 | 45 | this._jwtProvider = null; 46 | this._authorizationObserver = null; 47 | 48 | this._started = false; 49 | this._startPromise = null; 50 | 51 | this._authorizerCachePromise = null; 52 | this._authorizerCacheTimestamp = null; 53 | 54 | const requestInterceptor = RequestInterceptor.fromDelegate((options, endpoint) => { 55 | return Promise.resolve() 56 | .then(() => { 57 | return this._jwtProvider.getToken() 58 | .then((token) => { 59 | options.headers = options.headers || {}; 60 | options.headers.Authorization = `Bearer ${token}`; 61 | 62 | return options; 63 | }); 64 | }).catch((e) => { 65 | const failure = FailureReason.forRequest({ endpoint: endpoint }) 66 | .addItem(FailureType.REQUEST_IDENTITY_FAILURE) 67 | .format(); 68 | 69 | return Promise.reject(failure); 70 | }); 71 | }); 72 | 73 | const protocolType = Enum.fromCode(ProtocolType, protocol.toUpperCase()); 74 | 75 | this._userReadEndpoint = EndpointBuilder.for('read-user', 'read user data') 76 | .withVerb(VerbType.GET) 77 | .withProtocol(protocolType) 78 | .withHost(host) 79 | .withPort(port) 80 | .withPathBuilder((pb) => { 81 | pb.withLiteralParameter('version', 'v1') 82 | .withLiteralParameter('users', 'users'); 83 | }) 84 | .withRequestInterceptor(requestInterceptor) 85 | .withResponseInterceptor(ResponseInterceptor.DATA) 86 | .withErrorInterceptor(ErrorInterceptor.GENERAL) 87 | .endpoint; 88 | 89 | this._authorizeEndpoint = EndpointBuilder.for('authorize-operation', 'authorize operation for user') 90 | .withVerb(VerbType.POST) 91 | .withProtocol(protocolType) 92 | .withHost(host) 93 | .withPort(port) 94 | .withPathBuilder((pb) => { 95 | pb.withLiteralParameter('version', 'v1') 96 | .withLiteralParameter('authorizations', 'authorizations'); 97 | }) 98 | .withBody() 99 | .withRequestInterceptor(requestInterceptor) 100 | .withResponseInterceptor(ResponseInterceptor.DATA) 101 | .withErrorInterceptor(ErrorInterceptor.GENERAL) 102 | .endpoint; 103 | 104 | this._operationsReadEndpoint = EndpointBuilder.for('read-operations', 'get list of operations for a product') 105 | .withVerb(VerbType.GET) 106 | .withProtocol(protocolType) 107 | .withHost(host) 108 | .withPort(port) 109 | .withPathBuilder((pb) => { 110 | pb.withLiteralParameter('version', 'v1') 111 | .withLiteralParameter('operations', 'operations') 112 | .withVariableParameter('product', 'product', 'product', false); 113 | }) 114 | .withResponseInterceptor(ResponseInterceptor.DATA) 115 | .withErrorInterceptor(ErrorInterceptor.GENERAL) 116 | .endpoint; 117 | 118 | this._metadataReadEndpoint = EndpointBuilder.for('read-service-metadata', 'check version of entitlements service') 119 | .withVerb(VerbType.GET) 120 | .withProtocol(protocolType) 121 | .withHost(host) 122 | .withPort(port) 123 | .withPathBuilder((pb) => 124 | pb.withLiteralParameter('version', 'v1') 125 | .withLiteralParameter('service', 'service') 126 | .withLiteralParameter('version', 'version') 127 | ) 128 | .withResponseInterceptor(ResponseInterceptor.DATA) 129 | .withErrorInterceptor(ErrorInterceptor.GENERAL) 130 | .endpoint; 131 | } 132 | 133 | /** 134 | * A description of the environment (e.g. development, production, etc). 135 | * 136 | * @public 137 | * @return {String} 138 | */ 139 | get environment() { 140 | return this._environment; 141 | } 142 | 143 | /** 144 | * Attempts to establish a connection to the backend. This function should be invoked 145 | * immediately following instantiation. Once the resulting promise resolves, a 146 | * connection has been established and other instance methods can be used. 147 | * 148 | * @public 149 | * @param {JwtProvider} jwtProvider 150 | * @param {Boolean=} eager 151 | * @returns {Promise} 152 | */ 153 | connect(jwtProvider, eager) { 154 | return Promise.resolve() 155 | .then(() => { 156 | assert.argumentIsRequired(jwtProvider, 'jwtProvider', JwtProvider, 'JwtProvider'); 157 | assert.argumentIsOptional(eager, 'eager', Boolean); 158 | 159 | if (this._startPromise === null) { 160 | this._startPromise = Promise.resolve() 161 | .then(() => { 162 | this._started = true; 163 | 164 | this._jwtProvider = jwtProvider; 165 | 166 | let cachePromise; 167 | 168 | if (eager) { 169 | cachePromise = getAuthorizer.call(this); 170 | } else { 171 | cachePromise = Promise.resolve(); 172 | } 173 | 174 | return cachePromise.then(() => { 175 | return this; 176 | }); 177 | }).catch((e) => { 178 | this._started = false; 179 | this._startPromise = null; 180 | 181 | this._jwtProvider = null; 182 | 183 | throw e; 184 | }); 185 | } 186 | 187 | return this._startPromise; 188 | }); 189 | } 190 | 191 | /** 192 | * Returns an authorization to perform on operation for a specific product. 193 | * 194 | * @public 195 | * @param {String} operation 196 | * @param {Object=} data 197 | * @returns {Promise} 198 | */ 199 | authorize(operation, data) { 200 | return Promise.resolve() 201 | .then(() => { 202 | assert.argumentIsRequired(operation, 'operation', String); 203 | assert.argumentIsOptional(data, 'data', Object); 204 | 205 | checkStart.call(this); 206 | 207 | return getAuthorizer.call(this) 208 | .then((authorizer) => { 209 | const response = authorizer.authorize(operation, data || { }); 210 | 211 | if (response !== null) { 212 | return Promise.resolve(response); 213 | } else { 214 | const payload = { operation }; 215 | 216 | if (data) { 217 | payload.data = data; 218 | } 219 | 220 | return Gateway.invoke(this._authorizeEndpoint, payload); 221 | } 222 | }).then((result) => { 223 | if (this._authorizationObserver !== null) { 224 | this._authorizationObserver(result.request, result.response); 225 | } 226 | 227 | return result.response.authorized || false; 228 | }); 229 | }); 230 | } 231 | 232 | /** 233 | * Assigns the authorization observer. 234 | * 235 | * @public 236 | * @param {Callbacks.AuthorizationObserver} authorizationObserver 237 | */ 238 | registerAuthorizationObserver(authorizationObserver) { 239 | assert.argumentIsRequired(authorizationObserver, 'authorizationObserver', Function); 240 | 241 | if (this._authorizationObserver) { 242 | throw new Error('An authorization observer has already been bound'); 243 | } 244 | 245 | this._authorizationObserver = authorizationObserver; 246 | } 247 | 248 | /** 249 | * Retrieves user data, including roles and permissions. 250 | * 251 | * @public 252 | * @returns {Promise} 253 | */ 254 | readUser() { 255 | return Promise.resolve() 256 | .then(() => { 257 | checkStart.call(this); 258 | 259 | return Gateway.invoke(this._userReadEndpoint); 260 | }); 261 | } 262 | 263 | /** 264 | * Retrieves all possible operation for a given product. 265 | * 266 | * @public 267 | * @param {String=} product 268 | * @returns {Promise} 269 | */ 270 | readOperations(product) { 271 | return Promise.resolve() 272 | .then(() => { 273 | assert.argumentIsOptional(product, 'product', String); 274 | 275 | checkStart.call(this); 276 | 277 | const payload = { }; 278 | 279 | payload.product = product || '*'; 280 | 281 | return Gateway.invoke(this._operationsReadEndpoint, payload); 282 | }); 283 | } 284 | 285 | /** 286 | * Retrieves information regarding the remote service (e.g. version number, current user identifier, etc). 287 | * 288 | * @public 289 | * @returns {Promise} 290 | */ 291 | readServiceMetadata() { 292 | return Promise.resolve() 293 | .then(() => { 294 | checkStart.call(this); 295 | 296 | return Gateway.invoke(this._metadataReadEndpoint); 297 | }); 298 | } 299 | 300 | /** 301 | * Creates and starts a new {@link EntitlementsGateway} for use in the private development environment. 302 | * 303 | * @public 304 | * @static 305 | * @param {JwtProvider} jwtProvider 306 | * @param {Callbacks.AuthorizationObserver=} authorizationObserver 307 | * @param {Boolean=} eager 308 | * @returns {Promise} 309 | */ 310 | static forDevelopment(jwtProvider, authorizationObserver, eager) { 311 | return Promise.resolve() 312 | .then(() => { 313 | return start(new EntitlementsGateway(REST_API_SECURE_PROTOCOL, Configuration.developmentHost, REST_API_SECURE_PORT, 'development'), jwtProvider, authorizationObserver, eager); 314 | }); 315 | } 316 | 317 | /** 318 | * Creates and starts a new {@link EntitlementsGateway} for use in the private staging environment. 319 | * 320 | * @public 321 | * @static 322 | * @param {JwtProvider} jwtProvider 323 | * @param {Callbacks.AuthorizationObserver=} authorizationObserver 324 | * @param {Boolean=} eager 325 | * @returns {Promise} 326 | */ 327 | static forStaging(jwtProvider, authorizationObserver, eager) { 328 | return Promise.resolve() 329 | .then(() => { 330 | return start(new EntitlementsGateway(REST_API_SECURE_PROTOCOL, Configuration.stagingHost, REST_API_SECURE_PORT, 'staging'), jwtProvider, authorizationObserver, eager); 331 | }); 332 | } 333 | 334 | /** 335 | * Creates and starts a new {@link EntitlementsGateway} for use in the public production environment. 336 | * 337 | * @public 338 | * @static 339 | * @param {JwtProvider} jwtProvider 340 | * @param {Callbacks.AuthorizationObserver=} authorizationObserver 341 | * @param {Boolean=} eager 342 | * @returns {Promise} 343 | */ 344 | static forProduction(jwtProvider, authorizationObserver, eager) { 345 | return Promise.resolve() 346 | .then(() => { 347 | return start(new EntitlementsGateway(REST_API_SECURE_PROTOCOL, Configuration.productionHost, REST_API_SECURE_PORT, 'production'), jwtProvider, authorizationObserver, eager); 348 | }); 349 | } 350 | 351 | /** 352 | * Creates and starts a new {@link EntitlementsGateway} for use in the private admin environment. 353 | * 354 | * @public 355 | * @static 356 | * @param {JwtProvider} jwtProvider 357 | * @param {Callbacks.AuthorizationObserver=} authorizationObserver 358 | * @param {Boolean=} eager 359 | * @returns {Promise} 360 | */ 361 | static forAdmin(jwtProvider, authorizationObserver, eager) { 362 | return Promise.resolve() 363 | .then(() => { 364 | return start(new EntitlementsGateway(REST_API_SECURE_PROTOCOL, Configuration.adminHost, REST_API_SECURE_PORT, 'admin'), jwtProvider, authorizationObserver, eager); 365 | }); 366 | } 367 | 368 | _onDispose() { 369 | return; 370 | } 371 | 372 | toString() { 373 | return '[EntitlementsGateway]'; 374 | } 375 | } 376 | 377 | function start(gateway, jwtProvider, authorizationObserver, eager) { 378 | return gateway.connect(jwtProvider, eager) 379 | .then(() => { 380 | if (authorizationObserver) { 381 | gateway.registerAuthorizationObserver(authorizationObserver); 382 | } 383 | 384 | return gateway; 385 | }); 386 | } 387 | 388 | function checkStart() { 389 | if (this.getIsDisposed()) { 390 | throw new Error('Unable to use gateway, the gateway has been disposed.'); 391 | } 392 | 393 | if (!this._started) { 394 | throw new Error('Unable to use gateway, the gateway has not started.'); 395 | } 396 | } 397 | 398 | function getTimestamp() { 399 | const now = new Date(); 400 | 401 | return now.getTime(); 402 | } 403 | 404 | function getAuthorizer() { 405 | return Promise.resolve() 406 | .then(() => { 407 | if (this._authorizerCachePromise === null || this._authorizerCacheTimestamp === null || getTimestamp() > this._authorizerCacheTimestamp + REFRESH_INTERVAL_MILLISECONDS) { 408 | this._authorizerCachePromise = Promise.all([ 409 | this.readUser(), 410 | this.readOperations() 411 | ]).then((results) => { 412 | this._authorizerCacheTimestamp = getTimestamp(); 413 | 414 | return new Authorizer(results[0], results[1]); 415 | }); 416 | } 417 | 418 | return this._authorizerCachePromise; 419 | }); 420 | } 421 | 422 | class Authorizer { 423 | constructor(user, operations) { 424 | this._user = user; 425 | this._operations = operations; 426 | } 427 | 428 | authorize(operation, data) { 429 | const request = { }; 430 | 431 | request.user = { }; 432 | request.user.user = this._user.user; 433 | request.user.context = this._user.context; 434 | request.user.roles = this._user.roles.map(r => r.role); 435 | 436 | const o = this._operations.find(o => o.operation === operation); 437 | 438 | if (!o) { 439 | return null; 440 | } 441 | 442 | request.operation = { }; 443 | request.operation.operation = o.operation; 444 | request.operation.product = o.product; 445 | request.operation.restrictions = o.restrictions; 446 | 447 | request.data = data; 448 | 449 | const result = { }; 450 | 451 | result.request = request; 452 | result.response = { }; 453 | 454 | const permissions = this._user.roles.reduce((accumulator, role) => { 455 | role.permissions.forEach((permission) => { 456 | if (permission.operation.operation === operation) { 457 | accumulator.push(permission); 458 | } 459 | }); 460 | 461 | return accumulator; 462 | }, [ ]); 463 | 464 | if (permissions.length === 0) { 465 | result.response.authorized = false; 466 | result.response.advice = [ ]; 467 | 468 | return result; 469 | } 470 | 471 | const permissionsForRate = permissions.filter(p => p.restrictions.some(r => r.type === 'RATE_LIMITED')); 472 | 473 | if (permissions.length === permissionsForRate.length) { 474 | return null; 475 | } 476 | 477 | const permissionsForCount = permissions.filter(p => p.restrictions.some(r => r.type === 'COUNT_LIMITED')); 478 | 479 | if (permissions.length === permissionsForCount.length) { 480 | const restrictions = permissionsForCount.reduce((accumulator, permission) => { 481 | return accumulator.concat(permission.restrictions.filter(r => r.type === 'COUNT_LIMITED')); 482 | }, [ ]); 483 | 484 | restrictions.sort((a, b) => { 485 | return b.count - a.count; 486 | }); 487 | 488 | const restriction = restrictions[0]; 489 | 490 | let additional; 491 | 492 | if (is.number(data.count)) { 493 | additional = { actual: data.count }; 494 | } else { 495 | additional = null; 496 | } 497 | 498 | result.response.authorized = is.number(data.count) && restriction.count >= data.count; 499 | 500 | const advice = { }; 501 | 502 | advice.restriction = restriction; 503 | advice.restricted = !result.response.authorized; 504 | 505 | if (additional !== null) { 506 | advice.additional = additional; 507 | } 508 | 509 | result.response.advice = [ advice ]; 510 | 511 | return result; 512 | } else { 513 | result.response.authorized = true; 514 | result.response.advice = [ ]; 515 | 516 | return result; 517 | } 518 | } 519 | } 520 | 521 | return EntitlementsGateway; 522 | })(); 523 | --------------------------------------------------------------------------------