diff --git a/CHANGELOG.md b/CHANGELOG.md index e775f2e..271b840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and here ### Changed --> +## Unreleased — Authorize migrated legacy operations (#5) + +- Add authorized runtime API wrappers for cursor-based find, execution lookup, and restart. +- Qualify every migrated query through the trusted principal before engine or datastore access. + ## Unreleased — Require trusted workflow principals (#4) - Require authenticated principals for workflow execution operations. diff --git a/api-boundary.test.mjs b/api-boundary.test.mjs index a7bc0bd..4fefb76 100644 --- a/api-boundary.test.mjs +++ b/api-boundary.test.mjs @@ -23,3 +23,31 @@ test('API rejects calls without a trusted principal', async () => { const api = new BPMNAPI({ engine: { invoke: () => assert.fail('engine must not be called') } }); await assert.rejects(() => api.engine.invoke({}, {}), /authenticated principal is required/); }); + +test('migrated legacy operations qualify queries before reaching the engine or datastore', async () => { + const calls = []; + const principal = { + userName: 'alice', + qualifyItems: query => ({ ...query, authorizedItem: true }), + qualifyInstances: query => ({ ...query, authorizedInstance: true }) + }; + const api = new BPMNAPI({ + engine: { + get: query => { calls.push(['get', query]); return {}; }, + restart: (query, data, userName) => { calls.push(['restart', query, userName]); return {}; } + }, + dataStore: { + find: options => { calls.push(['find', options]); return {}; } + } + }); + + await api.engine.get({ id: 1 }, principal); + await api.engine.restart({ id: 2 }, {}, principal); + await api.data.find({ filter: { status: 'running' } }, principal); + + assert.deepEqual(calls, [ + ['get', { id: 1, authorizedItem: true }], + ['restart', { id: 2, authorizedItem: true }, 'alice'], + ['find', { filter: { status: 'running', authorizedInstance: true } }] + ]); +}); diff --git a/src/API/API.ts b/src/API/API.ts index 2f339af..61a5642 100644 --- a/src/API/API.ts +++ b/src/API/API.ts @@ -119,6 +119,11 @@ export interface IAPIEngine { */ startEvent(query, elementId, data: {}, user: ISecureUser, options?:IEngineOptions): Promise; +/** + * Retrieve one execution after applying the caller's item-level authorization. + */ + get(query, user: ISecureUser): Promise; + /** * @@ -131,7 +136,7 @@ export interface IAPIEngine { * @param inputData * */ - restart(itemQuery, data:any,userName, options?) :Promise; + restart(itemQuery, data:any, user: ISecureUser, options?) :Promise; /** * upgrade running instances with the latest revised bpmn model @@ -150,6 +155,10 @@ export interface IAPIEngine { export interface IAPIData { +/** + * Run a cursor-based instance query after qualifying its filter for the caller. + */ + find(options, user: ISecureUser); /** returns list of `User Tasks` that the user has access to @@ -261,9 +270,17 @@ class APIEngine extends APIComponent implements IAPIEngine { } public async startEvent(query, elementId, data = {}, user?: ISecureUser, options:IEngineOptions = {}): Promise { user=this.getUser(user); + query = user.qualifyItems(query); return await this.server.engine.startEvent(query, elementId, data,user.userName,options); } + public async get(query, user?: ISecureUser): Promise { + user=this.getUser(user); + query = user.qualifyItems(query); + return await this.server.engine.get(query); + } public async restart(itemQuery, data:any,user:ISecureUser, options={}) :Promise { + user=this.getUser(user); + itemQuery = user.qualifyItems(itemQuery); return await this.server.engine.restart(itemQuery, data,user.userName, options); } @@ -272,6 +289,11 @@ class APIEngine extends APIComponent implements IAPIEngine { } } class APIData extends APIComponent { + public async find(options: any = {}, user?: ISecureUser) { + user=this.getUser(user); + options = { ...options, filter: user.qualifyInstances(options.filter || {}) }; + return await this.server.dataStore.find(options); + } public async getPendingUserTasks(query, user?: ISecureUser): Promise { query['items.status'] = 'wait';