-
Notifications
You must be signed in to change notification settings - Fork 114
Expand file tree
/
Copy pathbaseOrgRepository.js
More file actions
1123 lines (1027 loc) · 47.8 KB
/
baseOrgRepository.js
File metadata and controls
1123 lines (1027 loc) · 47.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const BaseRepository = require('./baseRepository')
const BaseOrgModel = require('../model/baseorg')
const CNAOrgModel = require('../model/cnaorg')
const ADPOrgModel = require('../model/adporg')
const BulkDownloadModel = require('../model/bulkdownloadorg')
const SecretariatOrgModel = require('../model/secretariatorg')
const CveIdRepository = require('./cveIdRepository')
const uuid = require('uuid')
const _ = require('lodash')
const BaseOrg = require('../model/baseorg')
const ConversationRepository = require('./conversationRepository')
const getConstants = require('../constants').getConstants
const skipNulls = (objValue, srcValue) => {
if (_.isArray(objValue)) {
return srcValue
}
return undefined
}
/**
* @function setAggregateOrgObj
* @description Constructs the aggregation pipeline for legacy organization objects.
* @param {object} query - The query object to match.
* @returns {Array} The aggregation pipeline.
*/
function setAggregateOrgObj (query) {
return [
{
$match: query
},
{
$project: {
_id: false,
UUID: true,
short_name: true,
name: true,
'authority.active_roles': true,
'policies.id_quota': true,
time: true
}
}
]
}
/**
* @function setAggregateRegistryOrgObj
* @description Constructs the aggregation pipeline for registry organization objects.
* @param {object} query - The query object to match.
* @returns {Array} The aggregation pipeline.
*/
function setAggregateRegistryOrgObj (query) {
return [
{
$match: query
},
{
$project: {
_id: false,
__t: false,
inUse: false,
in_use: false
}
}
]
}
class BaseOrgRepository extends BaseRepository {
constructor () {
super(BaseOrg)
}
/**
* @async
* @function findOneByShortNameWithSelect
* @description Finds an organization by short name and selects specific fields.
* @param {string} shortName - The short name of the organization.
* @param {string} select - The fields to select.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [returnLegacyFormat=false] - If true, returns the legacy format.
* @returns {Promise<object|null>} The organization object.
*/
async findOneByShortNameWithSelect (shortName, select, options = {}, returnLegacyFormat = false) {
const OrgRepository = require('./orgRepository')
if (returnLegacyFormat) return await OrgRepository.findOneByShortName(shortName, options)
return await BaseOrgModel.findOne({ short_name: shortName }, null, options).select(select)
}
/**
* @async
* @function findOneByShortName
* @description Finds an organization by short name.
* @param {string} shortName - The short name of the organization.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [returnLegacyFormat=false] - If true, returns the legacy format.
* @returns {Promise<object|null>} The organization object.
*/
async findOneByShortName (shortName, options = {}, returnLegacyFormat = false) {
const OrgRepository = require('./orgRepository')
const legacyOrgRepo = new OrgRepository()
if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options)
const data = await BaseOrgModel.findOne({ short_name: shortName }, null, options)
return data
}
/**
* @async
* @function findOneByUUID
* @description Finds an organization by UUID.
* @param {string} UUID - The UUID of the organization.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [returnLegacyFormat=false] - If true, returns the legacy format.
* @returns {Promise<object|null>} The organization object.
*/
async findOneByUUID (UUID, options = {}, returnLegacyFormat = false) {
const OrgRepository = require('./orgRepository')
const legacyOrgRepo = new OrgRepository()
if (returnLegacyFormat) return await legacyOrgRepo.findOneByUUID(UUID, options)
return await BaseOrgModel.findOne({ UUID: UUID }, null, options)
}
/**
* @async
* @function getOrgUUID
* @description Retrieves the UUID of an organization by its short name.
* @param {string} shortName - The short name of the organization.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [useLegacy=false] - Unused parameter.
* @returns {Promise<string|null>} The organization UUID or null if not found.
*/
async getOrgUUID (shortName, options = {}, useLegacy = false) {
const org = await BaseOrgModel.findOne({ short_name: shortName }, null, options)
if (org) return org.UUID
return null
}
/**
* @async
* @function orgExists
* @description Checks if an organization exists by short name.
* @param {string} shortName - The short name of the organization.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [returnLegacyFormat=false] - If true, checks against legacy format.
* @returns {Promise<boolean>} True if the organization exists, false otherwise.
*/
async orgExists (shortName, options = {}, returnLegacyFormat = false) {
if (await this.findOneByShortName(shortName, options, returnLegacyFormat)) {
return true
}
return false
}
/**
* @async
* @function addUserToOrg
* @description Adds a user to an organization, optionally as an admin.
* @param {string} orgShortName - The short name of the organization.
* @param {string} userUUID - The UUID of the user to add.
* @param {boolean} [isAdmin=false] - If true, adds the user as an admin.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [isLegacyObject=false] - Unused parameter.
* @returns {Promise<void>}
*/
async addUserToOrg (orgShortName, userUUID, isAdmin = false, options = {}, isLegacyObject = false) {
const update = {
$addToSet: { users: userUUID }
}
if (isAdmin) {
update.$addToSet.admins = userUUID
}
await BaseOrgModel.updateOne({ short_name: orgShortName }, update, options)
}
/**
* @async
* @function addAdmin
* @description Adds a user to an organization's admin list.
* @param {string} orgShortName - The short name of the organization.
* @param {string} userUUID - The UUID of the user to add.
* @param {object} [options={}] - Optional settings for the repository query.
* @returns {Promise<void>}
*/
async addAdmin (orgShortName, userUUID, options = {}) {
const UserRepository = require('./userRepository')
const legacyUserRepo = new UserRepository()
const executeOptions = { ...options, new: true }
const updatedOrg = await BaseOrgModel.findOneAndUpdate(
{ short_name: orgShortName },
{ $addToSet: { admins: userUUID } },
executeOptions
)
await legacyUserRepo.collection.findOneAndUpdate(
{ UUID: userUUID },
{ $addToSet: { 'authority.active_roles': 'ADMIN' } },
options
)
return updatedOrg
}
/**
* @async
* @function removeAdmin
* @description Removes a user from an organization's admin list.
* @param {string} orgShortName - The short name of the organization.
* @param {string} userUUID - The UUID of the user to remove.
* @param {object} [options={}] - Optional settings for the repository query.
* @returns {Promise<void>}
*/
async removeAdmin (orgShortName, userUUID, options = {}) {
const UserRepository = require('./userRepository')
const legacyUserRepo = new UserRepository()
const executeOptions = { ...options, new: true }
const updatedOrg = await BaseOrgModel.findOneAndUpdate(
{ short_name: orgShortName },
{ $pull: { admins: userUUID } },
executeOptions
)
await legacyUserRepo.collection.findOneAndUpdate(
{ UUID: userUUID },
{ $pull: { 'authority.active_roles': 'ADMIN' } },
options
)
return updatedOrg
}
/**
* @async
* @function getAllOrgs
* @description Retrieves all organizations with pagination.
* @param {object} [options={}] - Pagination and query options.
* @param {boolean} [returnLegacyFormat=false] - If true, returns data in legacy format.
* @returns {Promise<object>} Paginated result containing organizations and metadata.
*/
async getAllOrgs (options = {}, returnLegacyFormat = false) {
const OrgRepository = require('./orgRepository')
const orgRepo = new OrgRepository()
let pg
if (returnLegacyFormat) {
const agt = setAggregateOrgObj({})
pg = await orgRepo.aggregatePaginate(agt, options)
} else {
const agt = setAggregateRegistryOrgObj({})
pg = await this.aggregatePaginate(agt, options)
}
const data = { organizations: pg.itemsList }
if (pg.itemCount >= options.limit) {
data.totalCount = pg.itemCount
data.itemsPerPage = pg.itemsPerPage
data.pageCount = pg.pageCount
data.currentPage = pg.currentPage
data.prevPage = pg.prevPage
data.nextPage = pg.nextPage
}
return data
}
/**
* @async
* @function getOrgObject
* @description Retrieves an organization object by identifier (UUID or short name).
* @param {string} identifier - The identifier (UUID or short name).
* @param {boolean} [identifierIsUUID=false] - True if identifier is a UUID.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [returnLegacyFormat=false] - If true, returns legacy format.
* @returns {Promise<object|null>} The organization object.
*/
async getOrgObject (identifier, identifierIsUUID = false, options = {}, returnLegacyFormat = false) {
const data = identifierIsUUID
? await this.findOneByUUID(identifier, options, returnLegacyFormat)
: await this.findOneByShortName(identifier, options, returnLegacyFormat)
if (!data) return null
return data
}
/**
* @async
* @function getOrg
* @description Retrieves a sanitized organization object.
* @param {string} identifier - The identifier (UUID or short name).
* @param {boolean} [identifierIsUUID=false] - True if identifier is a UUID.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [returnLegacyFormat=false] - If true, returns legacy format.
* @returns {Promise<object|null>} The sanitized organization object.
*/
async getOrg (identifier, identifierIsUUID = false, options = {}, returnLegacyFormat = false) {
const { deepRemoveEmpty } = require('../utils/utils')
const data = identifierIsUUID
? await this.findOneByUUID(identifier, options, returnLegacyFormat)
: await this.findOneByShortName(identifier, options, returnLegacyFormat)
if (!data) return null
const result = data.toObject()
delete result.__t
delete result.__v
delete result._id
delete result.inUse
delete result.in_use
return deepRemoveEmpty(result)
}
/**
* @async
* @function getOrgIdQuota
* @description Calculates the ID quota and availability for an organization.
* @param {object} org - The organization object.
* @param {boolean} [useLegacy=false] - If true, uses legacy policies for calculation.
* @returns {Promise<object>} Object containing id_quota/hard_quota, total_reserved, and available.
*/
async getOrgIdQuota (org, useLegacy = false) {
const returnPayload = {
...(useLegacy ? { id_quota: org.policies.id_quota } : { hard_quota: org.hard_quota }),
total_reserved: null,
available: null
}
const query = {
owning_cna: org.UUID,
state: getConstants().CVE_STATES.RESERVED
}
const cveIdRepo = new CveIdRepository()
const docs = await cveIdRepo.countDocuments(query)
returnPayload.total_reserved = docs
if (useLegacy) {
returnPayload.available = returnPayload.id_quota - returnPayload.total_reserved
} else {
returnPayload.available = returnPayload.hard_quota - returnPayload.total_reserved
}
return returnPayload
}
/**
* @async
* @function createOrg
* @description Creates a new organization in both the registry and a parallel legacy system. It handles the conversion between legacy and registry data formats, assigns a shared UUID, and saves the new organization to the respective data stores.
*
* @param {object} incomingOrg - The raw organization data object. Can be in either legacy or registry format, specified by the `isLegacyObject` flag.
* @param {object} [options={}] - Optional settings passed to the legacy repository for database operations.
* @param {boolean} [isLegacyObject=false] - If true, `incomingOrg` is treated as a legacy-formatted object. If false, it's treated as a registry-formatted object.
* @param {string|null} [requestingUserUUID=null] - The user UUID representing the requester, used for audit documentation. If null, no audit document is created.
* @param {boolean} [isSecretariat=false] - If true, the operation is performed by a Secretariat.
*
* @returns {Promise<object>} A promise that resolves to a plain JavaScript object representing the newly created organization. The format of the returned object (legacy or registry) is determined by the `isLegacyObject` parameter. The object is stripped of internal properties and empty values.
* @throws {string} Throws an error if the organization's authority role is not 'SECRETARIAT' or 'CNA'.
*/
async createOrg (incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isSecretariat = false) {
const { deepRemoveEmpty } = require('../utils/utils')
const OrgRepository = require('./orgRepository')
const CONSTANTS = getConstants()
// In the future we may be able to dynamically detect, but for now we will take a boolean
let legacyObjectRaw = null
let registryObjectRaw = null
let registryObject = null
const legacyOrgRepo = new OrgRepository()
const ReviewObjectRepository = require('./reviewObjectRepository')
const reviewObjectRepo = new ReviewObjectRepository()
// generate a shared uuid
const sharedUUID = uuid.v4()
if (isLegacyObject) {
legacyObjectRaw = incomingOrg
registryObjectRaw = this.convertLegacyToRegistry(incomingOrg)
} else {
registryObjectRaw = incomingOrg
legacyObjectRaw = this.convertRegistryToLegacy(incomingOrg)
}
if (!registryObjectRaw.authority) {
registryObjectRaw.authority = ['CNA']
}
if (!legacyObjectRaw.authority?.active_roles) {
legacyObjectRaw.authority = {
active_roles: ['CNA']
}
}
// Registry stuff
// Add uuid to org object
registryObjectRaw.UUID = sharedUUID
// Figure out why this is not working....
// registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value))
// Call Deep remove empty
registryObjectRaw = deepRemoveEmpty(registryObjectRaw)
// For all of these writes, if we are a secretariat, then we can write directly to the database, otherwise, we write to the review objects
// Write - use org type specific model
if (registryObjectRaw.authority.includes('SECRETARIAT')) {
// Write
// testing:
registryObjectRaw.authority = 'SECRETARIAT'
const SecretariatObjectToSave = new SecretariatOrgModel(registryObjectRaw)
if (isSecretariat) {
registryObject = await SecretariatObjectToSave.save(options)
} else {
await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options)
}
} else if (registryObjectRaw.authority.includes('CNA')) {
// A special case, we should make sure we have the default quota if it is not set
if (!registryObjectRaw.hard_quota) {
// set to default quota if none is specified
registryObjectRaw.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA
}
// Write
const CNAObjectToSave = new CNAOrgModel(registryObjectRaw)
if (isSecretariat) {
registryObject = await CNAObjectToSave.save(options)
} else {
await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options)
}
} else if (registryObjectRaw.authority.includes('ADP')) {
registryObjectRaw.hard_quota = 0
const adpObjectToSave = new ADPOrgModel(registryObjectRaw)
if (isSecretariat) {
registryObject = await adpObjectToSave.save(options)
} else {
await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options)
}
} else if (registryObjectRaw.authority.includes('BULK_DOWNLOAD')) {
registryObjectRaw.hard_quota = 0
const bulkDownloadObjectToSave = new BulkDownloadModel(registryObjectRaw)
if (isSecretariat) {
registryObject = await bulkDownloadObjectToSave.save(options)
} else {
await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options)
}
} else {
// Throw an Error instance so callers can catch and handle it properly
throw new Error("Unknown Org type requested. Please use either 'SECRETARIAT', 'CNA', 'ADP', or 'BULK_DOWNLOAD' as the authority role.")
}
// ADD AUDIT ENTRY AUTOMATICALLY for the registry object
if (requestingUserUUID) {
try {
const AuditRepository = require('./auditRepository')
const auditRepo = new AuditRepository()
await auditRepo.appendToAuditHistoryForOrg(
registryObjectRaw.UUID,
registryObjectRaw,
requestingUserUUID,
options
)
} catch (auditError) {
}
}
// Legacy Write, this will be removed when backwards compatibility is no longer needed.
legacyObjectRaw.UUID = sharedUUID
//* ******* Legacy has some special cases that we have to deal with here.**************
// Holy wow. This should be replaced with something in the future. This is NOT what you think it is
legacyObjectRaw.inUse = false
if (!legacyObjectRaw?.policies?.id_quota) {
// set to default quota if none is specified
_.set(legacyObjectRaw, 'policies.id_quota', CONSTANTS.DEFAULT_ID_QUOTA)
}
if (
legacyObjectRaw.authority.active_roles.length === 1 && (
legacyObjectRaw.authority.active_roles[0] === 'ADP' ||
legacyObjectRaw.authority.active_roles[0] === 'BULK_DOWNLOAD')
) {
// ADPs have quota of 0
_.set(legacyObjectRaw, 'policies.id_quota', 0)
}
// The legacy way of doing this, the way this is written under the hood there is no other way
// This await does not return a value, even though there is a return in it. :shrugg:
let postUpdate = {}
if (isSecretariat) {
delete legacyObjectRaw.time
postUpdate = await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options)
}
// If we are not a secretariat, then we need to return the uuid of the review object.
if (!isSecretariat) {
return {}
}
if (isLegacyObject) {
// This gets us the mongoose object that has all the right data in it, the "legacyObjectRaw" is the custom JSON we are sending. NOT the post written object.
// Convert the actual model, back to a json model
const legacyObjectRawJson = postUpdate.toObject()
// Remove private stuff
delete legacyObjectRawJson.__v
delete legacyObjectRawJson._id
delete legacyObjectRawJson.inUse
delete legacyObjectRawJson.in_use
return deepRemoveEmpty(legacyObjectRawJson)
}
const rawRegistryOrgObject = registryObject.toObject()
delete rawRegistryOrgObject.__t
delete rawRegistryOrgObject.__v
delete rawRegistryOrgObject._id
delete rawRegistryOrgObject.inUse
delete rawRegistryOrgObject.in_use
return deepRemoveEmpty(rawRegistryOrgObject)
}
/**
* @async
* @function updateOrg
* @description Updates an organization's details in both the new registry system and a parallel legacy system. It finds the organization by its short name, applies the provided updates, and saves the changes to both data sources.
*
* @param {string} shortName - The unique short name of the organization to update.
* @param {object} incomingParameters - An object containing the fields to update.
* @param {string} [incomingParameters.new_short_name] - The new short name for the organization. (Applied to both legacy and registry)
* @param {string} [incomingParameters.name] - The new long name for the organization. (Applied to both legacy and registry)
* @param {object} [incomingParameters.active_roles] - Object to manage active roles. (Applied to both legacy and registry)
* @param {string[]} [incomingParameters.active_roles.add] - An array of role strings to add.
* @param {string[]} [incomingParameters.active_roles.remove] - An array of role strings to remove.
* @param {number} [incomingParameters.id_quota] - The ID quota for the organization. (Applied to legacy and CNA-type registry orgs)
* @param {string} [incomingParameters.root_or_tlr] - The root or Top-Level Root (TLR) status. (Registry only)
* @param {string} [incomingParameters.charter_or_scope] - The charter or scope description. (Registry only)
* @param {string} [incomingParameters.disclosure_policy] - The disclosure policy. (Registry only)
* @param {string[]} [incomingParameters.product_list] - A list of the organization's products. (Registry only)
* @param {string[]} [incomingParameters.oversees] - A list of short names of organizations this org oversees. (Registry only)
* @param {string} [incomingParameters.reports_to] - The short name of the organization this org reports to. (Registry only)
* @param {string} [incomingParameters.contact_info.poc] - The primary point of contact's name. (Registry only)
* @param {string} [incomingParameters.contact_info.poc_email] - The primary point of contact's email. (Registry only)
* @param {string} [incomingParameters.contact_info.poc_phone] - The primary point of contact's phone number. (Registry only)
* @param {string} [incomingParameters.contact_info.org_email] - The general organization email address. (Registry only)
* @param {string} [incomingParameters.contact_info.website] - The organization's website URL. (Registry only)
* @param {string} [incomingParameters.cna_role_type] - (Registry only)
* @param {string} [incomingParameters.cna_country] - (Registry only)
* @param {string[]} [incomingParameters.vulnerability_advisory_locations] - (Registry only)
* @param {boolean} [incomingParameters.advisory_location_require_credentials] - (Registry only)
* @param {string} [incomingParameters.industry] - (Registry only)
* @param {string} [incomingParameters.tl_root_start_date] - (Registry only)
* @param {boolean} [incomingParameters.is_cna_discussion_list] - (Registry only)
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [isLegacyObject=false] - If true, the function returns the updated legacy organization object. Otherwise, it returns the updated registry organization object.
* @param {string|null} [requestingUserUUID=null] - The user UUID representing the requester, used for audit documentation. If null, no audit document is created.
* @param {boolean} [isAdmin=false] - If true, the operation is performed by an Admin.
* @param {boolean} [isSecretariat=false] - If true, the operation is performed by a Secretariat.
*
* @returns {Promise<object>} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values.
*/
async updateOrg (shortName, incomingParameters, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) {
const { deepRemoveEmpty } = require('../utils/utils')
const OrgRepository = require('./orgRepository')
// If we get here, we know the org exists
const legacyOrgRepo = new OrgRepository()
const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options)
let registryOrg = await this.findOneByShortName(shortName, options)
const originalRegistryOrgObject = registryOrg.toObject()
// Both legacy and registry
if (incomingParameters?.new_short_name) {
registryOrg.short_name = incomingParameters.new_short_name
legacyOrg.short_name = incomingParameters.new_short_name
}
registryOrg.long_name = incomingParameters?.name ?? registryOrg.long_name
legacyOrg.name = incomingParameters?.name ?? legacyOrg.name
// TODO: We should probably limit this so it only puts in things that we allow
const rolesToAdd = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.add'))).filter(role => getConstants().ORG_ROLES.includes(role))
const rolesToRemove = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.remove'))).filter(role => getConstants().ORG_ROLES.includes(role))
const initialRoles = legacyOrg.authority?.active_roles ?? []
const finalRoles = [...new Set([...initialRoles, ...rolesToAdd])].filter(role => !rolesToRemove.includes(role))
let roleChange = false
// Check if final roles match the original roles in the registry org
if (!_.isEqual(finalRoles.sort(), registryOrg.authority.sort())) {
roleChange = true
}
// Update authority and discriminator based on role changes
registryOrg.authority = finalRoles
// Determine the target model based on the new authority
let TargetModel = null
if (finalRoles.includes('SECRETARIAT')) {
TargetModel = SecretariatOrgModel
} else if (finalRoles.includes('CNA')) {
TargetModel = CNAOrgModel
} else if (finalRoles.includes('ADP')) {
TargetModel = ADPOrgModel
} else if (finalRoles.includes('BULK_DOWNLOAD')) {
TargetModel = BulkDownloadModel
}
// Save changes - handle possible model type change
if (TargetModel && roleChange) {
const oldId = registryOrg._id
// Remove the old document
await BaseOrgModel.deleteOne({ _id: oldId }, options)
// Create a new document of the correct type, preserving the UUID
const newDocData = registryOrg.toObject()
delete newDocData.__t
newDocData._id = oldId
const newDoc = new TargetModel(newDocData)
// Save the new document (validation will now use the correct schema)
await newDoc.save(options)
// Replace the reference so later code works with the newly saved document
registryOrg = newDoc
}
_.set(legacyOrg, 'authority.active_roles', finalRoles)
const directRegistryKeys = [
'root_or_tlr',
'charter_or_scope',
'disclosure_policy',
'product_list',
'oversees',
'reports_to',
'contact_info', // Handles all nested contact_info fields automatically
'cna_role_type',
'cna_country',
'vulnerability_advisory_locations',
'advisory_location_require_credentials',
'industry',
'tl_root_start_date',
'is_cna_discussion_list'
]
// Create a patch object by picking only the defined, relevant keys
// We filter out undefined values so _.merge doesn't overwrite existing fields with undefined
const registryUpdates = _.omitBy(
_.pick(incomingParameters, directRegistryKeys),
_.isUndefined
)
// Apply the patch object.
_.merge(registryOrg, registryUpdates)
// Registry Only Stuff
// Only a CNA object can have quota
if (registryOrg.__t === 'CNAOrg' && incomingParameters?.id_quota !== undefined) {
registryOrg.hard_quota = incomingParameters.id_quota
}
const legacyUpdates = {}
// legacy Only Stuff
if (incomingParameters.id_quota !== undefined) {
_.set(legacyUpdates, 'policies.id_quota', incomingParameters.id_quota)
}
_.merge(legacyOrg, legacyUpdates)
// ADD AUDIT ENTRY AUTOMATICALLY for the registry object before it gets saved.
if (requestingUserUUID) {
try {
const AuditRepository = require('./auditRepository')
const auditRepo = new AuditRepository()
// Seed the audit history with the existing org data if an audit document doesn't already exist.
// This is necessary because older entities might not have an audit log yet, and we want
// the first entry to be their baseline state before this update.
await auditRepo.seedAuditHistoryForOrg(
registryOrg.UUID,
originalRegistryOrgObject,
requestingUserUUID,
options
)
// Get the org state before save for comparison
const beforeUpdateObject = originalRegistryOrgObject
const afterUpdateObject = registryOrg.toObject()
// Clean objects for comparison (remove Mongoose metadata)
const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt'])
const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt'])
// Only add audit entry if there are changes
if (!_.isEqual(cleanBefore, cleanAfter)) {
await auditRepo.appendToAuditHistoryForOrg(
registryOrg.UUID,
registryOrg.toObject(),
requestingUserUUID,
options
)
}
} catch (auditError) {
}
}
// Save changes
await legacyOrg.save(options)
await registryOrg.save(options)
if (isLegacyObject) {
const plainJavascriptLegacyOrg = legacyOrg.toObject()
delete plainJavascriptLegacyOrg.__v
delete plainJavascriptLegacyOrg._id
delete plainJavascriptLegacyOrg.inUse
delete plainJavascriptLegacyOrg.in_use
return deepRemoveEmpty(plainJavascriptLegacyOrg)
}
const plainJavascriptRegistryOrg = registryOrg.toObject()
// Remove private things
delete plainJavascriptRegistryOrg.__v
delete plainJavascriptRegistryOrg._id
delete plainJavascriptRegistryOrg.__t
delete plainJavascriptRegistryOrg.inUse
delete plainJavascriptRegistryOrg.in_use
return deepRemoveEmpty(plainJavascriptRegistryOrg)
}
/**
* @function getJointApprovalFields
* @description Identifies fields requiring joint approval between original and updated organization objects.
* @param {object} orgObjectOriginal - The original organization object.
* @param {object} orgObjectUpdated - The updated organization object.
* @param {boolean} [isLegacyObject=false] - If true, checks legacy fields.
* @returns {string[]} List of fields that require joint approval.
*/
getJointApprovalFields (orgObjectOriginal, orgObjectUpdated, isLegacyObject = false) {
// Get the list of fields that require joint approval
let jointApprovalFields
if (isLegacyObject) {
jointApprovalFields = getConstants().JOINT_APPROVAL_FIELDS_LEGACY
} else {
jointApprovalFields = getConstants().JOINT_APPROVAL_FIELDS
}
// Filter the list to find only fields that have changed
const changedFields = _.filter(jointApprovalFields, field => {
// Check if the value in the original object is different from the updated object
return _.get(orgObjectOriginal, field) !== _.get(orgObjectUpdated, field)
})
// Return the array of fields that had changes (will be empty if none changed)
return changedFields
}
/**
/**
* @async
* @function updateOrgFull
* @description Updates an organization in both the registry and parallel legacy system using the provided full organization body. It finds the organization by its short name, applies the provided updates, and saves the changes to both data sources.
*
* @param {string} shortName - The short name of the organization to update.
* @param {object} incomingOrg - The body containing the full organization object to update.
* @param {object} [options={}] - Optional settings for the repository query.
* @param {boolean} [isLegacyObject=false] - If true, the function returns the updated legacy organization object. Otherwise, it returns the updated registry organization object.
* @param {string} [requestingUserUUID=null] - The user UUID representing the requester, used for audit documentation. If null, no audit document is created.
* @param {boolean} [isAdmin=false] - If true, the operation is performed by an Admin.
* @param {boolean} [isSecretariat=false] - If true, the operation is performed by a Secretariat.
*
* @returns {Promise<object>} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values.
*/
async updateOrgFull (shortName, incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) {
// TODO: Fix these imports, remove the circular imports
const { deepRemoveEmpty } = require('../utils/utils')
const OrgRepository = require('./orgRepository')
const ReviewObjectRepository = require('./reviewObjectRepository')
const BaseUserRepository = require('./baseUserRepository')
const legacyOrgRepo = new OrgRepository()
const reviewObjectRepo = new ReviewObjectRepository()
const userRepo = new BaseUserRepository()
const conversationRepo = new ConversationRepository()
const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options)
const registryOrg = await this.findOneByShortName(shortName, options)
const originalRegistryOrgObject = registryOrg.toObject()
// check to see if there is a PENDING review object:
const reviewObject = await reviewObjectRepo.getOrgReviewObjectByOrgShortname(shortName, isSecretariat, options)
const { conversation, ...incomingOrgBody } = incomingOrg
let legacyObjectRaw
let registryObjectRaw
if (isLegacyObject) {
legacyObjectRaw = incomingOrgBody
registryObjectRaw = this.convertLegacyToRegistry(incomingOrgBody)
} else {
registryObjectRaw = incomingOrgBody
legacyObjectRaw = this.convertRegistryToLegacy(incomingOrgBody)
}
if (incomingOrg?.new_short_name) {
const newName = incomingOrg.new_short_name
// 1. Update the Mongoose instances
registryOrg.short_name = newName
legacyOrg.short_name = newName
// 2. Update the raw tracking objects so lodash.merge doesn't restore the old short_name
registryObjectRaw.short_name = newName
legacyObjectRaw.short_name = newName
// 3. Remove new_short_name from the raw objects so it doesn't merge into the DB
delete registryObjectRaw.new_short_name
delete legacyObjectRaw.new_short_name
delete incomingOrg.new_short_name // Keeping for existing logic
}
// Checking for joint approval fields
const jointApprovalFieldsRegistry = this.getJointApprovalFields(registryOrg, registryObjectRaw)
const jointApprovalFieldsLegacy = this.getJointApprovalFields(legacyOrg, legacyObjectRaw, true)
let updatedRegistryOrg = null
let updatedLegacyOrg = null
let jointApprovalRegistry = null
// If there are no joint approval fields, merge the original and updated objects. Otherwise, update the registry object and legacy object separately considering joint approval.
// Dealing with roles requires a bit of extra control.
const originalRoles = registryOrg.authority
const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins']
if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) {
updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), legacyObjectRaw, skipNulls))
updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), protectedFields), registryObjectRaw, skipNulls))
} else {
// Check if there are actual changes to joint approval fields compared to current org object (not current review)
// Only compare fields that are actually in the incoming data
const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry))
const currentJointApprovalData = _.pick(registryOrg.toObject(), incomingJointApprovalKeys)
const incomingJointApprovalData = _.pick(registryObjectRaw, incomingJointApprovalKeys)
const hasJointApprovalChanges = !_.isEqual(currentJointApprovalData, incomingJointApprovalData)
if (hasJointApprovalChanges) {
// write the joint approval to the database
jointApprovalRegistry = _.merge({}, registryOrg.toObject(), registryObjectRaw)
if (reviewObject) {
await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options)
} else {
await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, options)
}
} else {
// If no changes between org and new object but a review object exists, remove it since joint approval is no longer needed
if (reviewObject) {
await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, options)
}
}
updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, jointApprovalFieldsRegistry), skipNulls))
updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, jointApprovalFieldsLegacy), skipNulls))
}
// handle conversation
const requestingUser = await userRepo.findUserByUUID(requestingUserUUID, options)
const conversationArray = []
if (conversation) {
conversationArray.push(await conversationRepo.createConversation(registryOrg.UUID, conversation, requestingUser, isSecretariat, options))
}
// ADD AUDIT ENTRY AUTOMATICALLY for the registry object before it gets saved.
if (requestingUserUUID) {
try {
const AuditRepository = require('./auditRepository')
const auditRepo = new AuditRepository()
// Seed the audit history with the existing org data if an audit document doesn't already exist.
// This is necessary because older entities might not have an audit log yet, and we want
// the first entry to be their baseline state before this update.
await auditRepo.seedAuditHistoryForOrg(
registryOrg.UUID,
originalRegistryOrgObject,
requestingUserUUID,
{ ...options, upsert: true }
)
// Get the org state before save for comparison
const beforeUpdateObject = originalRegistryOrgObject
const afterUpdateObject = registryOrg.toObject()
// Clean objects for comparison (remove Mongoose metadata)
const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt'])
const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt'])
// Only add audit entry if there are changes
if (!_.isEqual(cleanBefore, cleanAfter)) {
await auditRepo.appendToAuditHistoryForOrg(
registryOrg.UUID,
registryOrg.toObject(),
requestingUserUUID,
{ ...options, upsert: true }
)
}
console.log('Audit entry created for registry object')
} catch (auditError) {
console.error('Audit entry creation failed:', auditError)
}
}
// Handle possible authority (discriminator) changes that require a different Mongoose model
let roleChange = false
if (!_.isEqual([...originalRoles].sort(), [...updatedRegistryOrg?.authority].sort())) {
roleChange = true
}
// Determine the correct model based on the updated authority
let TargetModel = null
if (updatedRegistryOrg.authority?.includes('SECRETARIAT')) {
TargetModel = SecretariatOrgModel
} else if (updatedRegistryOrg.authority?.includes('CNA')) {
TargetModel = CNAOrgModel
} else if (updatedRegistryOrg.authority?.includes('ADP')) {
TargetModel = ADPOrgModel
} else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) {
TargetModel = BulkDownloadModel
}
// If the model type has changed, replace the document with a new one of the correct type
if (TargetModel && roleChange) {
const oldId = updatedRegistryOrg._id
// Remove the old document
await BaseOrgModel.deleteOne({ _id: oldId }, options)
// Prepare data for the new document, preserving the UUID and _id
const newDocData = updatedRegistryOrg.toObject()
delete newDocData.__t
newDocData._id = oldId
const newDoc = new TargetModel(newDocData)
await newDoc.save(options)
// Update reference so subsequent code works with the newly saved document
updatedRegistryOrg = newDoc
}
try {
await updatedLegacyOrg.save(options)
await updatedRegistryOrg.save(options)
} catch (error) {
throw new Error(`Failed to update organization ${shortName}. Error: ${error.message}`)
}
if (isLegacyObject) {
const plainJavascriptLegacyOrg = updatedLegacyOrg.toObject()
delete plainJavascriptLegacyOrg.__v
delete plainJavascriptLegacyOrg._id
delete plainJavascriptLegacyOrg.inUse
delete plainJavascriptLegacyOrg.in_use
plainJavascriptLegacyOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry))
return deepRemoveEmpty(plainJavascriptLegacyOrg)
}
const plainJavascriptRegistryOrg = updatedRegistryOrg.toObject()
plainJavascriptRegistryOrg.conversation = conversationArray
// Remove private things
delete plainJavascriptRegistryOrg.__v
delete plainJavascriptRegistryOrg._id
delete plainJavascriptRegistryOrg.__t
delete plainJavascriptRegistryOrg.inUse
delete plainJavascriptRegistryOrg.in_use
plainJavascriptRegistryOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry))
return deepRemoveEmpty(plainJavascriptRegistryOrg)
}
/**
* @async
* @function deleteOrg
* @description Deletes an organization in both the registry and parallel legacy system.
*
* @param {string} shortName - The short name of the organization to delete.
* @param {object} [options={}] - Optional settings for the repository query.
*
* @returns {Promise<void>}
*/
async deleteOrg (shortName, options = {}) {
const OrgRepository = require('./orgRepository')
const legacyOrgRepo = new OrgRepository()
await BaseOrgModel.deleteOne({ short_name: shortName }, options)
await legacyOrgRepo.deleteOneByShortName(shortName, options)
}
/**
* @function validateOrg
* @description Validates an organization object based on its authority roles.
* @param {object} org - The organization object to validate.
* @returns {object} The validation result object.
*/
validateOrg (org) {
if (!org.authority || (Array.isArray(org.authority) && org.authority.length === 0)) {
return { isValid: false, errors: [{ instancePath: '/authority', message: 'authority is required' }] }
}
let validateObject = {}
if (Array.isArray(org.authority)) {
// User passed in an array, we need to decide how we handle this.
if (org.authority.includes('SECRETARIAT')) {
org.authority = ['SECRETARIAT']
validateObject = SecretariatOrgModel.validateOrg(org)
} else {
// We are not a secretariat, so we need to take most priv
if (org.authority.includes('CNA') || org.authority.length === 0) {
org.authority = ['CNA']
validateObject = CNAOrgModel.validateOrg(org)
}
if (org.authority.includes('ADP')) {
org.authority = ['ADP']
validateObject = ADPOrgModel.validateOrg(org)
}
if (org.authority.includes('BULK_DOWNLOAD')) {
org.authority = ['BULK_DOWNLOAD']
validateObject = BulkDownloadModel.validateOrg(org)
}
}
} else {
if (org.authority === 'ADP') {
validateObject = ADPOrgModel.validateOrg(org)
}
if (org.authority === 'SECRETARIAT') {
validateObject = SecretariatOrgModel.validateOrg(org)
}