Skip to content
Merged
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
29 changes: 15 additions & 14 deletions lib/AbstractApiModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -481,27 +495,14 @@ 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)

let results = await this.find(req.apiData.query, opts, mongoOpts)

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)
Expand Down
Loading