Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions api-boundary.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }]
]);
});
24 changes: 23 additions & 1 deletion src/API/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ export interface IAPIEngine {
*/
startEvent(query, elementId, data: {}, user: ISecureUser, options?:IEngineOptions): Promise<IExecution>;

/**
* Retrieve one execution after applying the caller's item-level authorization.
*/
get(query, user: ISecureUser): Promise<IExecution>;


/**
*
Expand All @@ -131,7 +136,7 @@ export interface IAPIEngine {
* @param inputData
*
*/
restart(itemQuery, data:any,userName, options?) :Promise<IExecution>;
restart(itemQuery, data:any, user: ISecureUser, options?) :Promise<IExecution>;

/**
* upgrade running instances with the latest revised bpmn model
Expand All @@ -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

Expand Down Expand Up @@ -261,9 +270,17 @@ class APIEngine extends APIComponent implements IAPIEngine {
}
public async startEvent(query, elementId, data = {}, user?: ISecureUser, options:IEngineOptions = {}): Promise<IExecution> {
user=this.getUser(user);
query = user.qualifyItems(query);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve the authorized instance before calling startEvent

When a normal non-admin passes the string instance ID expected by Engine.startEvent, this line sends that primitive through SecureUser.qualifyItems, whose implementation assigns $or and optionally tenantId properties to its argument. In ESM strict mode this throws TypeError: Cannot create property '$or' on string ..., so authenticated non-admin callers can no longer start secondary events. Build and qualify an instance query, resolve its ID, and pass that ID to the engine instead.

Useful? React with 👍 / 👎.

return await this.server.engine.startEvent(query, elementId, data,user.userName,options);
}
public async get(query, user?: ISecureUser): Promise<IExecution> {
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<IExecution> {
user=this.getUser(user);
itemQuery = user.qualifyItems(itemQuery);
return await this.server.engine.restart(itemQuery, data,user.userName, options);

}
Expand All @@ -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 || {}) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Qualify the item returned by cursor searches

When a non-admin calls api.data.find with an items.* filter, qualifyInstances adds its authorization rules as a top-level $or, but Aggregate.find applies that $or before unwinding items while applying items.* predicates afterward. An instance containing both an Alice-visible item and a Bob-only item can therefore pass authorization through Alice's item and return Bob's item when the caller filters for it. Apply the authorization predicates to the same post-unwind item match, or prevent this wrapper from accepting item-level filters.

Useful? React with 👍 / 👎.

return await this.server.dataStore.find(options);
}
public async getPendingUserTasks(query, user?: ISecureUser): Promise<IItemData[]> {

query['items.status'] = 'wait';
Expand Down