Skip to content

Commit 859b0bd

Browse files
authored
fix: Removes references to organisations after deletion (LLC-26) (#1511)
1 parent 18fe639 commit 859b0bd

File tree

6 files changed

+231
-91
lines changed

6 files changed

+231
-91
lines changed

lib/models/organisation-test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,73 @@ import chai, { expect } from 'chai';
22
import chaiAsPromised from 'chai-as-promised';
33
import { Error } from 'mongoose';
44
import Organisation from './organisation';
5+
import User from './user';
6+
import Role from './role';
7+
import { createOrganisation, createUser } from './test-utils/testModelsHelper';
8+
import { userFixture } from './test-utils/fixtures/user.fixtures';
9+
import { orgFixture } from './test-utils/fixtures/organisation.fixtures';
510

611
chai.use(chaiAsPromised);
712

813
describe('organisation model', () => {
14+
afterEach(async () => {
15+
await Role.deleteMany({});
16+
await Organisation.deleteMany({});
17+
await User.deleteMany({});
18+
});
19+
20+
it('should properly create organisation', async () => {
21+
const user = await createUser(userFixture);
22+
const organisation = await createOrganisation(orgFixture);
23+
24+
const userAfterOrganisationCreated = await User.findOne({
25+
email: user.email
26+
});
27+
28+
const createdUsersOrganisation = userAfterOrganisationCreated
29+
.organisations
30+
.find(id => id.toString() === organisation._id.toString());
31+
const createdUsersOrganisationSetting = userAfterOrganisationCreated
32+
.organisationSettings
33+
.find(orgSetting =>
34+
orgSetting.organisation.toString() === organisation._id.toString()
35+
);
36+
const createdUsersRoles = await Role.find({
37+
organisation: organisation._id
38+
});
39+
40+
await expect(createdUsersOrganisation).to.not.be.undefined;
41+
await expect(createdUsersOrganisationSetting).to.not.be.undefined;
42+
await expect(createdUsersRoles).to.be.an('array').to.have.lengthOf(2);
43+
});
44+
45+
it('should properly delete organisation', async () => {
46+
const user = await createUser(userFixture);
47+
const organisation = await createOrganisation(orgFixture);
48+
49+
await organisation.remove();
50+
51+
const userAfterOrganisationDeleted = await User.findOne({
52+
email: user.email
53+
});
54+
55+
const deletedUsersOrganisation = userAfterOrganisationDeleted
56+
.organisations
57+
.find(id => id.toString() === organisation._id.toString());
58+
const deletedUsersOrganisationSetting = userAfterOrganisationDeleted
59+
.organisationSettings
60+
.find(orgSetting =>
61+
orgSetting.organisation.toString() === organisation._id.toString()
62+
);
63+
const deletedUsersRoles = await Role.find({
64+
organisation: organisation._id
65+
});
66+
67+
await expect(deletedUsersRoles).to.be.empty;
68+
await expect(deletedUsersOrganisation).to.be.undefined;
69+
await expect(deletedUsersOrganisationSetting).to.be.undefined;
70+
});
71+
972
it('should not throw a ValidationError when settings.LOCKOUT_ATTEMPS is larger than 0', async () => {
1073
const f = Organisation.create({
1174
settings: {

lib/models/organisation.js

Lines changed: 133 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import filterByOrg from 'lib/models/plugins/filterByOrg';
99
import keys from 'lodash/keys';
1010
import union from 'lodash/union';
1111
import map from 'lodash/map';
12-
import find from 'lodash/find';
1312
import isUndefined from 'lodash/isUndefined';
1413
import {
1514
VIEW_PUBLIC_DASHBOARDS,
@@ -22,8 +21,62 @@ import { update$dteTimezoneInDB } from 'lib/helpers/update$dteTimezoneInDB';
2221

2322
let Organisation;
2423

24+
/**
25+
* @typedef {object} ExpirationNotifications
26+
* @property {string} weekBeforeNotificationSent
27+
* @property {string} expirationNotificationSent
28+
*/
29+
30+
/**
31+
* @typedef {object} UsageStats
32+
* @property {Date} RUN_DATETIME
33+
* @property {Boolean} HAS_CHILDREN
34+
* @property {Number} OWN_COUNT
35+
* @property {Number} TOTAL_COUNT
36+
* @property {Number} OWN_ESTIMATED_BYTES
37+
* @property {Number} TOTAL_ESTIMATED_BYTES
38+
*/
39+
40+
/**
41+
* @typedef {object} Settings
42+
* @property {Boolean} LOCKOUT_ENABLED
43+
* @property {Number} LOCKOUT_ATTEMPS
44+
* @property {Number} LOCKOUT_SECONDS
45+
* @property {Boolean} PASSWORD_HISTORY_CHECK
46+
* @property {Number} PASSWORD_HISTORY_TOTAL
47+
* @property {Number} PASSWORD_MIN_LENGTH
48+
* @property {Boolean} PASSWORD_REQUIRE_ALPHA
49+
* @property {Boolean} PASSWORD_REQUIRE_NUMBER
50+
* @property {Boolean} PASSWORD_USE_CUSTOM_REGEX
51+
* @property {String} PASSWORD_CUSTOM_REGEX
52+
* @property {String} PASSWORD_CUSTOM_MESSAGE
53+
*/
54+
55+
/**
56+
* Plain object structure without mongoose model methods
57+
*
58+
* @typedef {object} Organisation
59+
* @property {string} name
60+
* @property {string} logoPath
61+
* @property {UploadSchema} logo TODO: define type
62+
* @property {string} customColors
63+
* @property {Organisation} parent
64+
* @property {*} owner TODO: define type
65+
* @property {Date} expiration
66+
* @property {ExpirationNotifications} expirationNotifications
67+
* @property {UsageStats} usageStats
68+
* @property {Settings} settings
69+
* @property {string} timezone
70+
*/
71+
72+
/** @typedef {module:mongoose.Model<Organisation>} OrganisationModel */
73+
74+
export const EMAIL_NOOP = 'EMAIL_NOOP';
75+
export const EMAIL_PROCESSING = 'EMAIL_PROCESSING';
76+
export const EMAIL_SENT = 'EMAIL_SENT';
77+
export const EMAIL_STATUS = [EMAIL_NOOP, EMAIL_PROCESSING, EMAIL_SENT];
78+
2579
async function validateLockoutAttempt(value) {
26-
// only validate field if lockout is enabled
2780
if (this.settings && this.settings.LOCKOUT_ENABLED) {
2881
if (value < 1) {
2982
throw new Error('A user should be allowed at least one attempt');
@@ -33,7 +86,6 @@ async function validateLockoutAttempt(value) {
3386
}
3487

3588
async function validateLockoutSeconds(value) {
36-
// only validate field if lockout is enabled
3789
if (this.settings && this.settings.LOCKOUT_ENABLED) {
3890
if (value < 5) {
3991
throw new Error('Must be at least 5 seconds');
@@ -43,7 +95,6 @@ async function validateLockoutSeconds(value) {
4395
}
4496

4597
async function validateHistoryTotal(value) {
46-
// only validate field if lockout is enabled
4798
if (this.settings && this.settings.LOCKOUT_ENABLED) {
4899
if (value < 1) {
49100
throw new Error('At least one password must be stored and checked with this setting enabled');
@@ -71,12 +122,6 @@ async function validateMinPasswordLength(value) {
71122
return true;
72123
}
73124

74-
export const EMAIL_NOOP = 'EMAIL_NOOP';
75-
export const EMAIL_PROCESSING = 'EMAIL_PROCESSING';
76-
export const EMAIL_SENT = 'EMAIL_SENT';
77-
78-
export const EMAIL_STATUS = [EMAIL_NOOP, EMAIL_PROCESSING, EMAIL_SENT];
79-
80125
const schema = new mongoose.Schema({
81126
name: { type: String },
82127
logoPath: { type: String },
@@ -105,15 +150,13 @@ const schema = new mongoose.Schema({
105150
},
106151
settings: {
107152
LOCKOUT_ENABLED: { type: Boolean, default: true },
108-
// number of attempts before lock out
109153
LOCKOUT_ATTEMPS: {
110154
type: Number,
111155
default: 5,
112156
validate: {
113157
validator: validateLockoutAttempt,
114158
},
115159
},
116-
// 30 minute lock out period
117160
LOCKOUT_SECONDS: {
118161
type: Number,
119162
default: 1800,
@@ -176,15 +219,20 @@ schema.plugin(auditRemove, {
176219
auditName: 'OrganisationAudit'
177220
});
178221

222+
/**
223+
* @param req
224+
* @param res
225+
* @param next
226+
*/
179227
schema.statics.readScopeChecks = function runReadScopeChecks(req, res, next) {
180228
// we should always be allowed to read orgs
181229
return next();
182230
};
183231

184232
/**
185233
* Returns an array of org ids that the given client can view limited to the level
186-
* @param {Object} orgIds ids to start with
187-
* @param {Object} totalIds ids found so far
234+
* @param {Object} stepIds ids to start with
235+
* @param {Object} cumulativeIds ids found so far
188236
* @param {Number} level How many levels deep to look for children
189237
* - default 0 (you can see your orgs)
190238
* @param {Function} cb Callback to be called with the result
@@ -210,108 +258,102 @@ schema.statics.limitIdsByOrg = function limitIdsByOrg(
210258
}
211259
};
212260

213-
schema.post('init', function handleInit() {
214-
this._original = this.toObject();
215-
});
216-
217-
schema.pre('save', async function preSave(next) {
218-
const User = getConnection().model('User');
261+
const createRoles = async (orgModel) => {
219262
const Role = getConnection().model('Role');
220263

221-
// https://github.com/Automattic/mongoose/issues/1474
222-
this.wasNew = this.isNew;
264+
await Role.create({
265+
title: 'Observer',
266+
owner_id: orgModel.owner,
267+
organisation: orgModel._id,
268+
scopes: [
269+
VIEW_PUBLIC_DASHBOARDS,
270+
VIEW_PUBLIC_VISUALISATIONS,
271+
VIEW_PUBLIC_JOURNEYS
272+
]
273+
});
274+
275+
return await Role.create({
276+
title: 'Admin',
277+
owner_id: orgModel.owner,
278+
organisation: orgModel._id,
279+
scopes: [ALL]
280+
});
281+
};
223282

224-
const organisation = this;
225-
const objectId = mongoose.Types.ObjectId;
283+
const updateOwner = async (orgModel, User, adminRole) => {
284+
const owner = await User.findById(orgModel.owner);
226285

227-
if (!isUndefined(this._oldExpiration) && this.expiration !== this._oldExpiration) {
228-
this.expirationNotifications.expirationNotificationSent = EMAIL_NOOP;
229-
this.expirationNotifications.weekBeforeNotificationSent = EMAIL_NOOP;
286+
if (!owner) {
287+
return;
230288
}
231289

232-
// https://github.com/Automattic/mongoose/issues/1474
233-
if (organisation.wasNew) {
234-
const createAdminRole = await Role.create({
235-
title: 'Admin',
236-
owner_id: organisation.owner,
237-
organisation: organisation._id,
238-
scopes: [ALL]
239-
});
240-
241-
const createObserverRole = await Role.create({
242-
title: 'Observer',
243-
owner_id: organisation.owner,
244-
organisation: organisation._id,
245-
scopes: [
246-
VIEW_PUBLIC_DASHBOARDS,
247-
VIEW_PUBLIC_VISUALISATIONS,
248-
VIEW_PUBLIC_JOURNEYS
249-
]
250-
});
290+
owner.organisations = union(owner.organisations || [], [orgModel._id]);
291+
owner.organisationSettings = union(
292+
owner.organisationSettings || [],
293+
[{
294+
organisation: orgModel._id,
295+
roles: [adminRole._id],
296+
}]
297+
);
251298

252-
const owner = await User.findById(objectId(organisation.owner));
253-
if (owner) {
254-
// add this org to the creator's allowed orgs
255-
const newRoles = [createAdminRole._id];
299+
await owner.save();
300+
};
256301

257-
const existingOrganisationSettings = owner.organisationSettings || [];
258-
let updatedSettings = existingOrganisationSettings;
302+
const createDependencies = async (orgModel, User) => {
303+
const adminRole = await createRoles(orgModel);
304+
await updateOwner(orgModel, User, adminRole);
305+
};
259306

260-
// check if the user has settings for this org
261-
const existingOS = find(existingOrganisationSettings, setting =>
262-
setting.organisation.toString() === organisation._id.toString()
263-
);
307+
const removeDependencies = async (orgModel) => {
308+
const Role = getConnection().model('Role');
309+
const User = getConnection().model('User');
264310

265-
if (existingOS) {
266-
const updatedOS = existingOS;
267-
// if the setting already exists, update it
268-
if (updatedOS.roles) {
269-
// union the roles if exist
270-
updatedOS.roles = union(updatedOS.roles, newRoles);
271-
} else {
272-
// set the new roles if no previous roles exist
273-
updatedOS.roles = newRoles;
274-
}
275-
276-
// loop through to create the updated settings array
277-
updatedSettings = map(updatedSettings, (os) => {
278-
if (os.organisation.toString() === organisation._id.toString()) {
279-
// return the updated settings into the array
280-
return updatedOS;
281-
}
282-
});
283-
} else {
284-
// insert a new item
285-
updatedSettings.push({
286-
organisation: organisation._id,
287-
roles: newRoles
288-
});
311+
await Role.deleteMany({ organisation: [orgModel._id] });
312+
await User.updateMany(
313+
{},
314+
{
315+
$pull: {
316+
organisations: orgModel._id,
317+
organisationSettings: { organisation: orgModel._id },
289318
}
319+
}
320+
);
321+
};
290322

291-
// update the settings on the owner
292-
owner.organisations = union(owner.organisations, [organisation._id]);
293-
owner.organisationSettings = updatedSettings;
294323

295-
await owner.save();
296-
}
324+
schema.post('init', function handleInit() {
325+
this._original = this.toObject();
326+
});
327+
328+
schema.pre('save', async function preSave(next) {
329+
const User = getConnection().model('User');
330+
331+
if (!isUndefined(this._oldExpiration) && this.expiration !== this._oldExpiration) {
332+
this.expirationNotifications.expirationNotificationSent = EMAIL_NOOP;
333+
this.expirationNotifications.weekBeforeNotificationSent = EMAIL_NOOP;
334+
}
297335

298-
await Promise.all([createAdminRole, createObserverRole]);
336+
if (!isUndefined(this._oldTimezone) && this.timezone !== this._oldTimezone) {
337+
await update$dteTimezoneInDB(this._id, this.timezone);
299338
}
300339

301-
// Update users settings
302340
await User.updateMany(
303-
{ ownerOrganisation: organisation._id },
341+
{ ownerOrganisation: this._id },
304342
{ ownerOrganisationSettings: this.settings },
305343
{ runValidators: false },
306344
);
307345

308-
// Update timezone offset of queries
309-
if (!isUndefined(this._oldTimezone) && this.timezone !== this._oldTimezone) {
310-
await update$dteTimezoneInDB(organisation._id, this.timezone);
346+
if (this.isNew) {
347+
await createDependencies(this, User);
311348
}
312349

313350
next();
314351
});
315352

353+
schema.post('remove', async (orgModel, next) => {
354+
await removeDependencies(orgModel);
355+
next();
356+
});
357+
316358
Organisation = getConnection().model('Organisation', schema, 'organisations');
317359
export default Organisation;

0 commit comments

Comments
 (0)