adapt-authoring-content 3.2.0 → 3.2.1
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.
- package/lib/ContentModule.js +11 -1
- package/package.json +1 -1
- package/tests/ContentModule.spec.js +55 -1
package/lib/ContentModule.js
CHANGED
|
@@ -665,8 +665,18 @@ class ContentModule extends AbstractApiModule {
|
|
|
665
665
|
const course = await this.findOne(
|
|
666
666
|
{ _type: 'course', _courseId },
|
|
667
667
|
{ validate: false },
|
|
668
|
-
{ projection: { updatedAt: 1 } }
|
|
668
|
+
{ projection: { updatedAt: 1, _type: 1, createdBy: 1, _isShared: 1, _shareWithUsers: 1, userGroups: 1 } }
|
|
669
669
|
)
|
|
670
|
+
if (!course) {
|
|
671
|
+
throw this.app.errors.NOT_FOUND.setData({ type: 'content', id: _courseId })
|
|
672
|
+
}
|
|
673
|
+
// this custom handler bypasses the standard per-item access check, so apply
|
|
674
|
+
// it here: the tree is readable only if the user can access the course
|
|
675
|
+
// (owner / _isShared / _shareWithUsers / shared group). 404 (not 403) so we
|
|
676
|
+
// don't leak the course's existence. Supers are exempt.
|
|
677
|
+
if (!req.auth.isSuper && this.accessCheckHook.hasObservers && !(await this.accessCheckHook.invoke(req, course)).every(Boolean)) {
|
|
678
|
+
throw this.app.errors.NOT_FOUND.setData({ type: 'content', id: _courseId })
|
|
679
|
+
}
|
|
670
680
|
const lastModified = new Date(course.updatedAt)
|
|
671
681
|
lastModified.setMilliseconds(0) // HTTP dates are second-precision; must match before comparing
|
|
672
682
|
const ifModifiedSince = req.headers['if-modified-since'] && new Date(req.headers['if-modified-since'])
|
package/package.json
CHANGED
|
@@ -560,6 +560,7 @@ describe('ContentModule', () => {
|
|
|
560
560
|
let statusCode
|
|
561
561
|
let ended = false
|
|
562
562
|
const req = {
|
|
563
|
+
auth: { isSuper: true },
|
|
563
564
|
apiData: { query: { _courseId: COURSE_ID } },
|
|
564
565
|
headers: { 'if-modified-since': new Date('2025-01-02T00:00:00Z').toUTCString() }
|
|
565
566
|
}
|
|
@@ -586,6 +587,7 @@ describe('ContentModule', () => {
|
|
|
586
587
|
find: mock.fn(async () => items)
|
|
587
588
|
})
|
|
588
589
|
const req = {
|
|
590
|
+
auth: { isSuper: true },
|
|
589
591
|
apiData: { query: { _courseId: COURSE_ID } },
|
|
590
592
|
headers: {}
|
|
591
593
|
}
|
|
@@ -617,13 +619,65 @@ describe('ContentModule', () => {
|
|
|
617
619
|
const inst = createInstance({
|
|
618
620
|
findOne: mock.fn(async () => { throw new Error('db error') })
|
|
619
621
|
})
|
|
620
|
-
const req = { apiData: { query: { _courseId: COURSE_ID } }, headers: {} }
|
|
622
|
+
const req = { auth: { isSuper: true }, apiData: { query: { _courseId: COURSE_ID } }, headers: {} }
|
|
621
623
|
const res = {}
|
|
622
624
|
const next = mock.fn()
|
|
623
625
|
await ContentModule.prototype.handleTree.call(inst, req, res, next)
|
|
624
626
|
assert.equal(next.mock.callCount(), 1)
|
|
625
627
|
assert.equal(next.mock.calls[0].arguments[0].message, 'db error')
|
|
626
628
|
})
|
|
629
|
+
|
|
630
|
+
it('should run the per-item access check for non-super users and return the tree when allowed', async () => {
|
|
631
|
+
const lastModified = new Date('2025-01-15T00:00:00Z')
|
|
632
|
+
const course = { _id: COURSE_ID, _type: 'course', updatedAt: lastModified }
|
|
633
|
+
const accessCheckHook = { hasObservers: true, invoke: mock.fn(async () => [true]) }
|
|
634
|
+
const inst = createInstance({
|
|
635
|
+
accessCheckHook,
|
|
636
|
+
findOne: mock.fn(async () => course),
|
|
637
|
+
find: mock.fn(async () => [{ _id: COURSE_ID, _type: 'course', _courseId: COURSE_ID }])
|
|
638
|
+
})
|
|
639
|
+
const req = { auth: { isSuper: false, user: {} }, apiData: { query: { _courseId: COURSE_ID } }, headers: {} }
|
|
640
|
+
const res = { set: mock.fn(), json: mock.fn() }
|
|
641
|
+
const next = mock.fn()
|
|
642
|
+
await ContentModule.prototype.handleTree.call(inst, req, res, next)
|
|
643
|
+
|
|
644
|
+
assert.equal(accessCheckHook.invoke.mock.callCount(), 1, 'access check invoked')
|
|
645
|
+
assert.deepEqual(accessCheckHook.invoke.mock.calls[0].arguments, [req, course], 'check receives req + the course')
|
|
646
|
+
assert.equal(inst.find.mock.callCount(), 1)
|
|
647
|
+
assert.equal(next.mock.callCount(), 0)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('should 404 (not leak existence) when the access check denies the course', async () => {
|
|
651
|
+
const accessCheckHook = { hasObservers: true, invoke: mock.fn(async () => [false]) }
|
|
652
|
+
const inst = createInstance({
|
|
653
|
+
accessCheckHook,
|
|
654
|
+
findOne: mock.fn(async () => ({ _id: COURSE_ID, _type: 'course', updatedAt: new Date() })),
|
|
655
|
+
app: { errors: { NOT_FOUND: { setData: () => new Error('NOT_FOUND') } } }
|
|
656
|
+
})
|
|
657
|
+
const req = { auth: { isSuper: false, user: {} }, apiData: { query: { _courseId: COURSE_ID } }, headers: {} }
|
|
658
|
+
const next = mock.fn()
|
|
659
|
+
await ContentModule.prototype.handleTree.call(inst, req, {}, next)
|
|
660
|
+
|
|
661
|
+
assert.equal(next.mock.callCount(), 1)
|
|
662
|
+
assert.equal(next.mock.calls[0].arguments[0].message, 'NOT_FOUND')
|
|
663
|
+
assert.equal(inst.find.mock.callCount(), 0, 'should not fetch items for an inaccessible course')
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('should skip the access check for super users', async () => {
|
|
667
|
+
const accessCheckHook = { hasObservers: true, invoke: mock.fn(async () => [false]) }
|
|
668
|
+
const inst = createInstance({
|
|
669
|
+
accessCheckHook,
|
|
670
|
+
findOne: mock.fn(async () => ({ _id: COURSE_ID, _type: 'course', updatedAt: new Date('2025-01-15T00:00:00Z') })),
|
|
671
|
+
find: mock.fn(async () => [])
|
|
672
|
+
})
|
|
673
|
+
const req = { auth: { isSuper: true }, apiData: { query: { _courseId: COURSE_ID } }, headers: {} }
|
|
674
|
+
const res = { set: mock.fn(), json: mock.fn() }
|
|
675
|
+
const next = mock.fn()
|
|
676
|
+
await ContentModule.prototype.handleTree.call(inst, req, res, next)
|
|
677
|
+
|
|
678
|
+
assert.equal(accessCheckHook.invoke.mock.callCount(), 0, 'super bypasses the access check')
|
|
679
|
+
assert.equal(next.mock.callCount(), 0)
|
|
680
|
+
})
|
|
627
681
|
})
|
|
628
682
|
|
|
629
683
|
describe('enforceAssetNotInUse', () => {
|