diff --git a/UnattendedMode.d.ts b/UnattendedMode.d.ts new file mode 100644 index 0000000..96adf3a --- /dev/null +++ b/UnattendedMode.d.ts @@ -0,0 +1,11 @@ +import { DataContext } from "./types"; +import {ConfigurationBase} from '@themost/common'; + +export declare interface ConfigurableApplication { + getConfiguration(): ConfigurationBase +} + +export declare function executeInUnattendedMode(context: DataContext, func: (callback: (err?: Error) => void) => void, callback: (err?: Error) => void): void; +export declare function executeInUnattendedModeAsync(context: DataContext, func: () => Promise): Promise; +export declare function enableUnattendedExecution(app: ConfigurableApplication, account?: string): void; +export declare function disableUnattendedExecution(app: ConfigurableApplication): void; diff --git a/UnattendedMode.js b/UnattendedMode.js new file mode 100644 index 0000000..462bee2 --- /dev/null +++ b/UnattendedMode.js @@ -0,0 +1,110 @@ +var unattendedMode = Symbol('unattendedMode'); +var {RandomUtils} = require('@themost/common'); + +/** + * Execute a callable function with elevated privileges in unattended mode. + * @param {import('./types').DataContext} context + * @param {function(function(Error?): void)} callable + * @param {function(Error?): void} callback + * @returns {void} + */ +function executeInUnattendedMode(context, callable, callback) { + if (typeof callable !== 'function') { + return callback(new Error('Unattended mode requires a callable function')); + } + // if the context is already in unattended mode + if (context[unattendedMode]) { + try { + // execute callable function + void callable(function (err) { + return callback(err); + }); + } catch (err) { + return callback(err); + } + } else { + var interactiveUser; + try { + const account = context.getConfiguration().getSourceAt('settings/auth/unattendedExecutionAccount'); + if (account == null) { + return callback(new Error('The unattended execution account is not defined. The operation cannot be completed.')); + } + // enter unattended mode + context[unattendedMode] = true; + // get interactive user + if (context.user) { + interactiveUser = Object.assign({}, context.user); + // set interactive user + context.interactiveUser = interactiveUser; + } + if (account) { + context.user = {name: account, authenticationType: 'Basic'}; + } + void callable(function (err) { + // restore user + if (interactiveUser) { + context.user = Object.assign({}, interactiveUser); + } + delete context.interactiveUser; + // exit unattended mode + delete context[unattendedMode]; + return callback(err); + }); + } catch (err) { + // restore user + if (interactiveUser) { + context.user = Object.assign({}, interactiveUser); + } + delete context.interactiveUser; + // exit unattended mode + delete context[unattendedMode]; + return callback(err); + } + } +} + +/** + * Execute a callable function with elevated privileges in unattended mode. + * @param {import('./types').DataContext} context + * @param {function(): Promise} callable + * @returns {Promise} + */ +function executeInUnattendedModeAsync(context, callable) { + return new Promise((resolve, reject) => { + void executeInUnattendedMode(context, function (cb) { + return callable().then(function () { + return cb(); + }).catch(function (err) { + return cb(err); + }); + }, function (err) { + if (err) { + return reject(err); + } + return resolve(); + }); + }); +} + +/** + * Enables unattended mode + * @param {{getConfiguration(): import('@themost/common').ConfigurationBase}} app + * @param {string=} executionAccount + */ +function enableUnattendedExecution(app, executionAccount) { + app.getConfiguration().setSourceAt('settings/auth/unattendedExecutionAccount', executionAccount || RandomUtils.randomChars(14)); +} +/** + * Disables unattended mode + * @param {{getConfiguration(): import('@themost/common').ConfigurationBase}} app + */ +function disableUnattendedExecution(app) { + app.getConfiguration().setSourceAt('settings/auth/unattendedExecutionAccount', null); +} + +module.exports = { + executeInUnattendedMode, + executeInUnattendedModeAsync, + enableUnattendedExecution, + disableUnattendedExecution +} diff --git a/index.d.ts b/index.d.ts index 5890499..e4b34ef 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,3 +20,4 @@ export * from './data-listeners'; export * from './data-associations'; export * from './data-application'; export * from './data-errors'; +export * from './UnattendedMode'; diff --git a/index.js b/index.js index fea3116..61223a6 100644 --- a/index.js +++ b/index.js @@ -85,7 +85,9 @@ var { DataObjectAssociationListener, DataObjectMultiAssociationError } = require('./data-associations'); var { DataApplication } = require('./data-application'); -var { UknonwnAttributeError } = require('./data-errors'); +var { UnknownAttributeError } = require('./data-errors'); + +var { executeInUnattendedModeAsync, executeInUnattendedMode, enableUnattendedExecution, disableUnattendedExecution } = require('./UnattendedMode'); module.exports = { TypeParser, @@ -168,6 +170,10 @@ module.exports = { ODataModelBuilder, ODataConventionModelBuilder, EntitySetSchemaLoaderStrategy, - UknonwnAttributeError + UnknownAttributeError, + enableUnattendedExecution, + disableUnattendedExecution, + executeInUnattendedMode, + executeInUnattendedModeAsync }; diff --git a/jest.config.js b/jest.config.js index 95dfff8..e284210 100644 --- a/jest.config.js +++ b/jest.config.js @@ -81,7 +81,11 @@ module.exports = { // ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, + moduleNameMapper: { + '^@themost/data$': [ + '/index' + ] + }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], diff --git a/package-lock.json b/package-lock.json index 5062d7b..a1e11b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@themost/data", - "version": "2.6.58", + "version": "2.6.60", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@themost/data", - "version": "2.6.56", + "version": "2.6.60", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.3.0", diff --git a/package.json b/package.json index 22acae2..30bf77c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/data", - "version": "2.6.58", + "version": "2.6.60", "description": "MOST Web Framework Codename Blueshift - Data module", "main": "index.js", "types": "index.d.ts", diff --git a/spec/UnattendedMode.spec.ts b/spec/UnattendedMode.spec.ts new file mode 100644 index 0000000..fdf2d7c --- /dev/null +++ b/spec/UnattendedMode.spec.ts @@ -0,0 +1,100 @@ +import {TestApplication} from './TestApplication'; +import { + DataContext, + disableUnattendedExecution, + enableUnattendedExecution, + executeInUnattendedModeAsync +} from '@themost/data'; +import {resolve} from 'path'; + +describe('UnattendedMode', () => { + let app: TestApplication; + let context: DataContext; + beforeAll((done) => { + app = new TestApplication(resolve(__dirname, 'test2')); + context = app.createContext(); + return done(); + }); + afterAll(async () => { + await context.finalizeAsync(); + await app.finalize(); + }); + it('should execute in unattended mode', async () => { + // set context user + context.user = { + name: 'angela.parry@example.com' + }; + const items = await context.model('Order').where('orderedItem/name').equal( + 'Razer Blade (2013)' + ).getItems(); + expect(items.length).toBeFalsy(); + await executeInUnattendedModeAsync(context, async () => { + expect(context.interactiveUser).toBeTruthy(); + expect(context.interactiveUser.name).toEqual('angela.parry@example.com'); + const items = await context.model('Order').where('orderedItem/name').equal( + 'Razer Blade (2013)' + ).getItems(); + expect(items.length).toBeTruthy(); + }); + expect(context.interactiveUser).toBeFalsy(); + }); + + it('should execute in unattended mode and get error', async () => { + // set context user + context.user = { + name: 'angela.parry@example.com' + }; + await expect(executeInUnattendedModeAsync(context, async () => { + throw new Error('Custom error'); + })).rejects.toThrow(); + expect(context.interactiveUser).toBeFalsy(); + expect(context.user.name).toEqual('angela.parry@example.com'); + }); + + it('should execute in unattended mode in series', async () => { + // set context user + context.user = { + name: 'angela.parry@example.com' + }; + await executeInUnattendedModeAsync(context, async () => { + expect(context.interactiveUser).toBeTruthy(); + const items = await context.model('Order').where('orderedItem/name').equal( + 'Razer Blade (2013)' + ).getItems(); + expect(items.length).toBeTruthy(); + await executeInUnattendedModeAsync(context, async () => { + expect(context.interactiveUser).toBeTruthy(); + expect(context.interactiveUser.name).toEqual('angela.parry@example.com'); + const items = await context.model('Order').where('orderedItem/name').equal( + 'Sony VAIO Flip 15' + ).getItems(); + expect(items.length).toBeTruthy(); + }); + }); + expect(context.interactiveUser).toBeFalsy(); + expect(context.user.name).toEqual('angela.parry@example.com'); + }); + + it('should disable unattended execution', async () => { + // set context user + context.user = { + name: 'angela.parry@example.com' + }; + disableUnattendedExecution(app); + expect(app.getConfiguration().getSourceAt('settings/auth/unattendedExecutionAccount')).toBeFalsy(); + await expect(executeInUnattendedModeAsync(context, async () => { + await context.model('Order').where('orderedItem/name').equal( + 'Sony VAIO Flip 15' + ).getItems(); + })).rejects.toThrow('The unattended execution account is not defined. The operation cannot be completed.'); + expect(context.interactiveUser).toBeFalsy(); + enableUnattendedExecution(app); + expect(app.getConfiguration().getSourceAt('settings/auth/unattendedExecutionAccount')).toBeTruthy(); + await executeInUnattendedModeAsync(context, async () => { + const items = await context.model('Order').where('orderedItem/name').equal( + 'Razer Blade (2013)' + ).getItems(); + expect(items.length).toBeTruthy(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 67a825e..b271c14 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,9 @@ "dom" ], "paths": { + "@themost/data": [ + "./index" + ] } }, "exclude": [