adapt-authoring-adaptframework 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -304,9 +304,13 @@ class AdaptFrameworkImport {
304
304
  }
305
305
  // find and store the course data path
306
306
  const courseDirs = await glob(`${this.path}/*/course`)
307
+ if (courseDirs.length === 0) {
308
+ this.framework.log('error', 'NO_COURSE_DIR', this.path)
309
+ throw App.instance.errors.FW_IMPORT_INVALID_COURSE.setData({ reason: 'no source course directory found in archive; expected a course folder nested one level deep' })
310
+ }
307
311
  if (courseDirs.length > 1) {
308
312
  this.framework.log('error', 'MULTIPLE_COURSE_DIRS', courseDirs)
309
- throw App.instance.errors.FW_IMPORT_INVALID_COURSE
313
+ throw App.instance.errors.FW_IMPORT_INVALID_COURSE.setData({ reason: 'multiple course directories found in archive; expected exactly one' })
310
314
  }
311
315
  this.coursePath = courseDirs[0]
312
316
  try {
@@ -4,7 +4,7 @@ import AdaptFrameworkImport from './AdaptFrameworkImport.js'
4
4
  import fs from 'node:fs/promises'
5
5
  import { getHandler, postHandler, importHandler, postUpdateHandler, getUpdateHandler } from './handlers.js'
6
6
  import { loadRouteConfig, registerRoutes } from 'adapt-authoring-server'
7
- import { runCliCommand, readFrameworkPluginVersions, migrateExistingCourses, computePluginHash, prebuildCache } from './utils.js'
7
+ import { applyContentAccessFilter, runCliCommand, readFrameworkPluginVersions, migrateExistingCourses, computePluginHash, prebuildCache } from './utils.js'
8
8
  import BuildCache from './BuildCache.js'
9
9
  import path from 'node:path'
10
10
  import semver from 'semver'
@@ -79,6 +79,7 @@ class AdaptFrameworkModule extends AbstractModule {
79
79
  this._targetFrameworkVersion = meta.framework?.targetVersion
80
80
 
81
81
  this.app.waitForModule('content').then(content => {
82
+ content.accessQueryHook.tap(this.onContentAccessQueryHook.bind(this))
82
83
  content.accessCheckHook.tap(this.checkContentAccess.bind(this))
83
84
  })
84
85
 
@@ -319,6 +320,18 @@ class AdaptFrameworkModule extends AbstractModule {
319
320
  schemas.forEach(s => jsonschema.registerSchema(s))
320
321
  }
321
322
 
323
+ /**
324
+ * Merges ownership/sharing access clauses into the content query when listing courses,
325
+ * so the database returns only courses the user can see. Non-course queries fall through
326
+ * to the per-item `checkContentAccess` safety net (the parent course's `_id` is not in
327
+ * scope at this stage).
328
+ * @param {external:ExpressRequest} req
329
+ */
330
+ async onContentAccessQueryHook (req) {
331
+ if (req.apiData.query._type !== 'course') return
332
+ applyContentAccessFilter(req.apiData.query, req.auth.user._id.toString())
333
+ }
334
+
322
335
  /**
323
336
  * Checks whether the request user should be given access to the content they're requesting
324
337
  * @param {external:ExpressRequest} req
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Mutates a mongo content query so it only matches courses the given user can access:
3
+ * owned by them, marked public via `_isShared`, or in `_shareWithUsers`. Combines safely
4
+ * with an existing `$or` (e.g. from search) by lifting both into `$and`.
5
+ * @param {object} query The mongo query object to mutate
6
+ * @param {string} userId The user's `_id` (already coerced to string)
7
+ * @memberof adaptframework
8
+ */
9
+ export function applyContentAccessFilter (query, userId) {
10
+ if (!userId) return
11
+ const clauses = [
12
+ { createdBy: userId },
13
+ { _isShared: true },
14
+ { _shareWithUsers: userId }
15
+ ]
16
+ if (query.$or) {
17
+ query.$and = [
18
+ ...(query.$and ?? []),
19
+ { $or: query.$or },
20
+ { $or: clauses }
21
+ ]
22
+ delete query.$or
23
+ } else {
24
+ query.$or = clauses
25
+ }
26
+ }
package/lib/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { applyContentAccessFilter } from './utils/applyContentAccessFilter.js'
1
2
  export { inferBuildAction } from './utils/inferBuildAction.js'
2
3
  export { getPluginUpdateStatus } from './utils/getPluginUpdateStatus.js'
3
4
  export { getImportContentCounts } from './utils/getImportContentCounts.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-adaptframework",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Adapt framework integration for the Adapt authoring tool",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-adaptframework",
6
6
  "license": "GPL-3.0",
@@ -0,0 +1,52 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { applyContentAccessFilter } from '../lib/utils/applyContentAccessFilter.js'
4
+
5
+ describe('applyContentAccessFilter()', () => {
6
+ it('should be a no-op when userId is falsy', () => {
7
+ const query = { _type: 'course' }
8
+ applyContentAccessFilter(query, undefined)
9
+ assert.deepEqual(query, { _type: 'course' })
10
+ })
11
+
12
+ it('should add a $or clause covering creator, public, and per-user sharing', () => {
13
+ const query = { _type: 'course' }
14
+ applyContentAccessFilter(query, 'user1')
15
+ assert.deepEqual(query.$or, [
16
+ { createdBy: 'user1' },
17
+ { _isShared: true },
18
+ { _shareWithUsers: 'user1' }
19
+ ])
20
+ })
21
+
22
+ it('should preserve an existing $or by lifting both into $and', () => {
23
+ const query = { $or: [{ title: 'foo' }] }
24
+ applyContentAccessFilter(query, 'user1')
25
+ assert.equal(query.$or, undefined)
26
+ assert.deepEqual(query.$and, [
27
+ { $or: [{ title: 'foo' }] },
28
+ {
29
+ $or: [
30
+ { createdBy: 'user1' },
31
+ { _isShared: true },
32
+ { _shareWithUsers: 'user1' }
33
+ ]
34
+ }
35
+ ])
36
+ })
37
+
38
+ it('should append to an existing $and rather than clobber it', () => {
39
+ const query = { $or: [{ title: 'foo' }], $and: [{ flag: true }] }
40
+ applyContentAccessFilter(query, 'user1')
41
+ assert.equal(query.$or, undefined)
42
+ assert.equal(query.$and.length, 3)
43
+ assert.deepEqual(query.$and[0], { flag: true })
44
+ })
45
+
46
+ it('should leave other top-level keys intact', () => {
47
+ const query = { _type: 'course', title: { $regex: 'foo' } }
48
+ applyContentAccessFilter(query, 'user1')
49
+ assert.equal(query._type, 'course')
50
+ assert.deepEqual(query.title, { $regex: 'foo' })
51
+ })
52
+ })