diff --git a/lib/AbstractApiModule.js b/lib/AbstractApiModule.js index 9e1676e7..9e1dfb7a 100644 --- a/lib/AbstractApiModule.js +++ b/lib/AbstractApiModule.js @@ -100,9 +100,22 @@ class AbstractApiModule extends AbstractModule { * `undefined` and any non-truthy return are treated as `false`. * Single-doc requests that are denied by any observer respond `401 Unauthorised`. * List requests silently filter denied items. + * + * Runs post-query, so it is best reserved as a safety net for checks that cannot be expressed + * as a query. For filtering, prefer `accessQueryHook` — filtering at this stage produces short + * pages and breaks skip-based pagination for any caller that relies on response length to + * detect end-of-results. * @type {Hook} */ this.accessCheckHook = new Hook() + /** + * Hook invoked before a query runs, allowing observers to merge access-control clauses into + * `req.apiData.query`. Preferred over `accessCheckHook` for filtering: keeps pagination counts + * and the `Link` header accurate, and avoids any need to top up short pages. + * Skipped for super users so they see unfiltered results, matching `checkAccess` behaviour. + * @type {Hook} + */ + this.accessQueryHook = new Hook() await this.setValues() this.validateValues() @@ -394,6 +407,7 @@ class AbstractApiModule extends AbstractModule { let data try { await this.requestHook.invoke(req) + if (!req.auth.isSuper) await this.accessQueryHook.invoke(req) const preCheck = method !== 'get' && method !== 'post' const postCheck = method === 'get' if (preCheck) { @@ -481,6 +495,7 @@ class AbstractApiModule extends AbstractModule { Object.keys(req.apiData.query).forEach(key => delete opts[key]) await this.requestHook.invoke(req) + if (!req.auth.isSuper) await this.accessQueryHook.invoke(req) await this.setUpPagination(req, res, mongoOpts) @@ -488,20 +503,6 @@ class AbstractApiModule extends AbstractModule { results = await this.checkAccess(req, results) - // If checkAccess filtered some results, fetch more to fill the page - const pageSize = mongoOpts.limit - if (pageSize && results.length < pageSize) { - let fetchSkip = mongoOpts.skip + pageSize - while (results.length < pageSize) { - const extra = await this.find(req.apiData.query, opts, { ...mongoOpts, skip: fetchSkip }) - if (!extra.length) break - const filtered = await this.checkAccess(req, extra) - results = results.concat(filtered) - fetchSkip += extra.length - } - if (results.length > pageSize) results = results.slice(0, pageSize) - } - results = await this.sanitise(req.apiData.schemaName, results, { isInternal: true, strict: false }) res.status(this.mapStatusCode('get')).json(results)