apostrophe 2.221.2 → 2.223.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.223.0 (2022-11-28)
4
+
5
+ ### Adds
6
+
7
+ * Allows to exclude certain groups of users from seeing a piece or a page in `Permissions` tab.
8
+
9
+ ### Fixes
10
+
11
+ * Handles joins in array fields correctly under high concurrent load. Previously it was possible for joins in array fields to fail to load under certain conditions.
12
+
13
+ ## 2.222.0 (2022-07-20)
14
+
15
+ ## Adds
16
+
17
+ * `testModule: true` is now compatible with `mocha` 10.x as well as previously supported versions of `mocha`. Thanks to [Amin Shazrin](https://github.com/ammein) for this contribution.
18
+
19
+ ## Fixes
20
+
21
+ * `sanitize-html` dependency bumped to ensure a potential denial-of-service vector is closed.
22
+
3
23
  ## 2.221.2 (2022-07-06)
4
24
 
5
25
  ## Fixes
package/index.js CHANGED
@@ -256,7 +256,7 @@ module.exports = function(options) {
256
256
  while (m.parent) {
257
257
  // The test file is the root as far as we are concerned,
258
258
  // not mocha itself
259
- if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
259
+ if (m.parent.filename.match(new RegExp(`${path.sep}node_modules${path.sep}mocha${path.sep}`))) {
260
260
  return m;
261
261
  }
262
262
  m = m.parent;
@@ -415,20 +415,18 @@ module.exports = function(options) {
415
415
  // and throws an exception if we don't
416
416
  function findTestModule() {
417
417
  var m = module;
418
- var nodeModuleRegex;
419
- if (process.platform === "win32") {
420
- nodeModuleRegex = /node_modules\\mocha/;
421
- } else {
422
- nodeModuleRegex = /node_modules\/mocha/;
418
+ var nodeModuleRegex = new RegExp("node_modules" + path.sep + "mocha");
419
+ if (!require.main.filename.match(nodeModuleRegex)) {
420
+ throw new Error('mocha does not seem to be running, is this really a test?');
423
421
  }
424
422
  while (m) {
425
423
  if (m.parent && m.parent.filename.match(nodeModuleRegex)) {
426
424
  return m;
425
+ } else if (!m.parent) {
426
+ // Mocha v10 doesn't inject mocha paths inside `module`, therefore, we only detect the parent until the last parent. But we can get Mocha running using `require.main` - Amin
427
+ return m;
427
428
  }
428
429
  m = m.parent;
429
- if (!m) {
430
- throw new Error('mocha does not seem to be running, is this really a test?');
431
- }
432
430
  }
433
431
  }
434
432
  }
@@ -59,6 +59,12 @@ module.exports = {
59
59
  value: 'certainUsers',
60
60
  label: 'Certain People',
61
61
  showFields: [ '_viewGroups', '_viewUsers' ]
62
+ },
63
+ {
64
+ value: 'excludeCertainUsers',
65
+ label: 'Exclude Certain People',
66
+ help: 'Selected group members will not be allowed to view the document. Please note that it will still require a logged-in user to view it, logged-out users will be excluded.',
67
+ showFields: [ '_excludeViewGroups' ]
62
68
  }
63
69
  ]
64
70
  },
@@ -80,6 +86,15 @@ module.exports = {
80
86
  sortable: false,
81
87
  editDocs: false
82
88
  },
89
+ {
90
+ name: '_excludeViewGroups',
91
+ type: 'joinByArray',
92
+ withType: 'apostrophe-group',
93
+ label: 'These Groups cannot View',
94
+ idsField: 'excludeViewGroupsIds',
95
+ sortable: false,
96
+ editDocs: false
97
+ },
83
98
  {
84
99
  name: '_editUsers',
85
100
  type: 'joinByArray',
@@ -336,6 +336,7 @@ module.exports = function(self, options) {
336
336
  var fields = {
337
337
  viewGroupsIds: 'view',
338
338
  viewUsersIds: 'view',
339
+ excludeViewGroupsIds: 'exclude-view',
339
340
  editGroupsIds: 'edit',
340
341
  editUsersIds: 'edit'
341
342
  };
@@ -947,7 +948,7 @@ module.exports = function(self, options) {
947
948
  // etc.
948
949
 
949
950
  self.docUnversionedFields = function(req, doc, fields) {
950
- fields.push('slug', 'docPermissions', 'viewUserIds', 'viewGroupIds', 'editUserIds', 'editGroupIds', 'loginRequired');
951
+ fields.push('slug', 'docPermissions', 'viewUserIds', 'viewGroupIds', 'excludeViewGroupIds', 'editUserIds', 'editGroupIds', 'loginRequired');
951
952
  };
952
953
 
953
954
  // Lock the given doc id to a given `contextId`, such
@@ -227,7 +227,7 @@ module.exports = function(self, options) {
227
227
 
228
228
  var admin = req.user && req.user._permissions.admin;
229
229
 
230
- var allowed = [ 'view' ];
230
+ var allowed = [ 'view', 'exclude-view' ];
231
231
  if (admin) {
232
232
  allowed.push('edit');
233
233
  }
@@ -313,6 +313,7 @@ module.exports = function(self, options) {
313
313
  'loginRequired',
314
314
  'viewUsersIds',
315
315
  'viewGroupsIds',
316
+ 'excludeViewGroupsIds',
316
317
  'editUsersIds',
317
318
  'editGroupsIds',
318
319
  'docPermissions'
@@ -58,6 +58,12 @@ module.exports = function(self, options) {
58
58
  if (req.user && _.intersection(self.userPermissionNames(req.user, 'edit'), object.docPermissions).length) {
59
59
  return true;
60
60
  }
61
+
62
+ // Case #5: object is restricted to exclude certain people
63
+ if (req.user && object.published && (object.loginRequired === 'excludeCertainUsers') && _.intersection(self.userPermissionNames(req.user, 'exclude-view'), object.docPermissions).length === 0) {
64
+ return true;
65
+ }
66
+
61
67
  } else {
62
68
  // Not view permissions
63
69
 
@@ -95,7 +101,6 @@ module.exports = function(self, options) {
95
101
  }
96
102
 
97
103
  // Case #3: doc is restricted to certain people
98
-
99
104
  clauses.push({
100
105
  published: true,
101
106
  loginRequired: 'certainUsers',
@@ -121,6 +126,13 @@ module.exports = function(self, options) {
121
126
  }
122
127
  ]
123
128
  });
129
+
130
+ // Case #5: doc is restricted to exclude certain people
131
+ clauses.push({
132
+ published: true,
133
+ loginRequired: 'excludeCertainUsers',
134
+ docPermissions: { $nin: self.userPermissionNames(req.user, 'exclude-view') }
135
+ });
124
136
  }
125
137
  } else {
126
138
  // Not view permissions
@@ -698,9 +698,11 @@ module.exports = {
698
698
  var joins = [];
699
699
 
700
700
  function findJoins(schema, arrays) {
701
+ // Shallow clone at the end to ensure our _dotPath and _arrays
702
+ // properties are unique to this request
701
703
  var _joins = _.filter(schema, function(field) {
702
704
  return !!self.fieldTypes[field.type].join;
703
- });
705
+ }).map(join => _.clone(join));
704
706
  _.each(_joins, function(join) {
705
707
  if (!arrays.length) {
706
708
  join._dotPath = join.name;
@@ -2736,7 +2738,7 @@ module.exports = {
2736
2738
  // Return all standard field names currently associated with permissions editing,
2737
2739
  // for consistency in arrangeFields, batch permissions schemas, etc.
2738
2740
  self.getPermissionsFieldNames = function() {
2739
- return [ 'loginRequired', '_viewUsers', '_viewGroups', '_editUsers', '_editGroups', 'applyToSubpages' ];
2741
+ return [ 'loginRequired', '_viewUsers', '_viewGroups', '_excludeViewGroups', '_editUsers', '_editGroups', 'applyToSubpages' ];
2740
2742
  };
2741
2743
 
2742
2744
  }
@@ -803,6 +803,7 @@ module.exports = {
803
803
  var fields = {
804
804
  'viewUsersIds': [],
805
805
  'viewGroupsIds': [],
806
+ 'excludeViewGroupsIds': [],
806
807
  'editUsersIds': [],
807
808
  'editGroupsIds': []
808
809
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "2.221.2",
3
+ "version": "2.223.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -72,7 +72,7 @@
72
72
  "request-promise": "^4.2.4",
73
73
  "resolve": "^1.20.0",
74
74
  "rimraf": "^2.7.1",
75
- "sanitize-html": "^2.4.0",
75
+ "sanitize-html": "^2.7.1",
76
76
  "server-destroy": "^1.0.1",
77
77
  "sluggo": "^0.2.0",
78
78
  "syntax-error": "^1.3.0",
@@ -0,0 +1,98 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+ let apos;
4
+
5
+ describe('Concurrent Array Joins', function() {
6
+
7
+ this.timeout(t.timeout);
8
+
9
+ after(function(done) {
10
+ return t.destroy(apos, done);
11
+ });
12
+
13
+ // EXISTENCE
14
+
15
+ it('should be a property of the apos object', function(done) {
16
+ apos = require('../index.js')({
17
+ root: module,
18
+ shortName: 'test',
19
+
20
+ modules: {
21
+ 'apostrophe-express': {
22
+ secret: 'xxx',
23
+ port: 7900
24
+ },
25
+ 'test-people': {
26
+ extend: 'apostrophe-pieces',
27
+ name: 'test-person',
28
+ alias: 'persons',
29
+ addFields: [
30
+ {
31
+ name: 'hobbies',
32
+ type: 'array',
33
+ schema: [
34
+ {
35
+ type: 'string',
36
+ name: 'name'
37
+ },
38
+ {
39
+ type: 'joinByOne',
40
+ name: '_friend',
41
+ withType: 'test-person'
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ afterInit: function(callback) {
49
+ assert(apos.docs);
50
+ apos.argv._ = [];
51
+ return callback(null);
52
+ },
53
+ afterListen: function(err) {
54
+ assert(!err);
55
+ done();
56
+ }
57
+ });
58
+ });
59
+
60
+ it('should be able to retrieve hobbies in parallel with all joins', async function() {
61
+ const req = apos.tasks.getReq();
62
+ const hobbyists = [];
63
+ for (let i = 0; (i < 10); i++) {
64
+ hobbyists.push(await apos.persons.insert(req, {
65
+ title: `Hobbyist ${i}`,
66
+ published: true
67
+ }));
68
+ }
69
+ for (let i = 0; (i < 10); i++) {
70
+ hobbyists[i].hobbies = [
71
+ {
72
+ name: `Hobby ${i}`,
73
+ friendId: hobbyists[9 - i]._id
74
+ }
75
+ ];
76
+ await apos.persons.update(req, hobbyists[i]);
77
+ }
78
+ const promises = [];
79
+ for (let i = 0; (i < 100); i++) {
80
+ promises.push(apos.persons.find(req).toArray());
81
+ }
82
+ const results = await Promise.all(promises);
83
+ assert.strictEqual(results.length, 100);
84
+ for (const result of results) {
85
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
86
+ result.sort((a, b) => a.title.localeCompare(b.title));
87
+ assert.strictEqual(result.length, 10);
88
+ for (let i = 0; (i < 10); i++) {
89
+ const person = result[i];
90
+ assert.strictEqual(person.title, `Hobbyist ${i}`);
91
+ assert.strictEqual(person.hobbies.length, 1);
92
+ assert.strictEqual(person.hobbies[0].name, `Hobby ${i}`);
93
+ assert(person.hobbies[0]._friend);
94
+ assert.strictEqual(person.hobbies[0]._friend.title, `Hobbyist ${9 - i}`);
95
+ }
96
+ }
97
+ });
98
+ });
@@ -86,6 +86,15 @@ describe('Permissions', function() {
86
86
  it('certainUsers will not let you slide past to an unpublished doc', function() {
87
87
  assert(!apos.permissions.can(req({ user: { _id: 1 } }), 'view-doc', { loginRequired: 'certainUsers', docPermissions: [ 'view-1' ] }));
88
88
  });
89
+ it('forbids view-doc for individual with group id', function() {
90
+ assert(!apos.permissions.can(req({ user: { _id: 1, groupIds: [ 1001, 1002 ] } }), 'view-doc', { published: true, loginRequired: 'excludeCertainUsers', docPermissions: [ 'exclude-view-1002' ] }));
91
+ });
92
+ it('permits view-doc for individual with wrong group id', function() {
93
+ assert(apos.permissions.can(req({ user: { _id: 2, groupIds: [ 1001, 1002 ] } }), 'view-doc', { published: true, loginRequired: 'excludeCertainUsers', docPermissions: [ 'exclude-view-1003' ] }));
94
+ });
95
+ it('excludeCertainUsers will not let you slide past to an unpublished doc', function() {
96
+ assert(!apos.permissions.can(req({ user: { _id: 1 } }), 'exclude-view-doc', { loginRequired: 'excludeCertainUsers', docPermissions: [ 'exclude-view-1' ] }));
97
+ });
89
98
  it('permits view-doc for unpublished doc for individual with group id for editing', function() {
90
99
  assert(apos.permissions.can(req({ user: { _id: 1, groupIds: [ 1001, 1002 ] } }), 'view-doc', { docPermissions: [ 'edit-1002' ] }));
91
100
  });
package/test/schemas.js CHANGED
@@ -153,6 +153,7 @@ var realWorldCase = {
153
153
  "loginRequired",
154
154
  "_viewUsers",
155
155
  "_viewGroups",
156
+ "_excludeViewGroups",
156
157
  "_editUsers",
157
158
  "_editGroups"
158
159
  ],