@webex/internal-plugin-ediscovery 2.59.2 → 2.59.3-next.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/src/transforms.js CHANGED
@@ -1,819 +1,819 @@
1
- import {requestWithRetries} from './retry';
2
-
3
- /**
4
- * This class is used to encrypt/decrypt various properties on ReportRequests, Activities and Spaces as they are sent/returned to/from the eDiscovery Service
5
- */
6
- class Transforms {
7
- /**
8
- * This function is used to encrypt sensitive properties on the ReportRequest before it is sent to the eDiscovery Service createReport API
9
- * @param {Object} ctx - An object containing a webex instance and a transform
10
- * @param {Object} object - Generic object that you want to encrypt some property on based on the type
11
- * @returns {Promise} - Returns a transform promise
12
- */
13
- static encryptReportRequest(ctx, object) {
14
- if (!object || !object.body) {
15
- return Promise.resolve(object);
16
- }
17
- const reportRequest = object.body;
18
-
19
- return ctx.webex.internal.encryption.kms
20
- .createUnboundKeys({count: 1})
21
- .then((keys) => {
22
- if (keys && keys.length > 0 && keys[0]) {
23
- reportRequest.encryptionKeyUrl = keys[0].uri;
24
-
25
- return ctx.webex.internal.encryption.kms
26
- .createResource({userIds: [keys[0].userId], keys})
27
- .then(() => {
28
- const promises = [];
29
-
30
- if (reportRequest.name) {
31
- promises.push(
32
- ctx.webex.internal.encryption
33
- .encryptText(keys[0], reportRequest.name)
34
- .then((encryptedName) => {
35
- reportRequest.name = encryptedName;
36
- })
37
- );
38
- }
39
-
40
- if (reportRequest.description) {
41
- promises.push(
42
- ctx.webex.internal.encryption
43
- .encryptText(keys[0], reportRequest.description)
44
- .then((encryptedDescription) => {
45
- reportRequest.description = encryptedDescription;
46
- })
47
- );
48
- }
49
-
50
- if (reportRequest.spaceNames) {
51
- promises.push(
52
- Promise.all(
53
- reportRequest.spaceNames.map((spaceName) =>
54
- ctx.webex.internal.encryption.encryptText(keys[0], spaceName)
55
- )
56
- ).then((encryptedSpaceNames) => {
57
- reportRequest.spaceNames = encryptedSpaceNames;
58
- })
59
- );
60
- }
61
-
62
- if (reportRequest.keywords) {
63
- promises.push(
64
- Promise.all(
65
- reportRequest.keywords.map((keyword) =>
66
- ctx.webex.internal.encryption.encryptText(keys[0], keyword)
67
- )
68
- ).then((encryptedKeywords) => {
69
- reportRequest.keywords = encryptedKeywords;
70
- })
71
- );
72
- }
73
-
74
- if (reportRequest.emails) {
75
- // store unencrypted emails for ediscovery service to convert to user ids
76
- reportRequest.unencryptedEmails = reportRequest.emails;
77
- promises.push(
78
- Promise.all(
79
- reportRequest.emails.map((email) =>
80
- ctx.webex.internal.encryption.encryptText(keys[0], email)
81
- )
82
- ).then((encryptedEmails) => {
83
- reportRequest.emails = encryptedEmails;
84
- })
85
- );
86
- }
87
-
88
- return Promise.all(promises);
89
- });
90
- }
91
-
92
- return Promise.resolve(object);
93
- })
94
- .catch((reason) => {
95
- ctx.webex.logger.error(
96
- `Error while encrypting report request: ${reportRequest} : ${reason}`
97
- );
98
-
99
- return Promise.reject(reason);
100
- });
101
- }
102
-
103
- /**
104
- * This function is used to decrypt encrypted properties on the ReportRequest that is returned from the eDiscovery Service getReport(s) API
105
- * @param {Object} ctx - An object containing a webex instance and a transform
106
- * @param {Object} object - Generic object that you want to decrypt some property on based on the type
107
- * @returns {Promise} - Returns a transform promise
108
- */
109
- static decryptReportRequest(ctx, object) {
110
- if (
111
- !object ||
112
- !object.body ||
113
- !object.body.reportRequest ||
114
- !object.body.reportRequest.encryptionKeyUrl
115
- ) {
116
- return Promise.resolve(object);
117
- }
118
- const {reportRequest} = object.body;
119
-
120
- let reportNamePromise;
121
-
122
- if (reportRequest.name) {
123
- reportNamePromise = ctx.webex.internal.encryption
124
- .decryptText(reportRequest.encryptionKeyUrl, reportRequest.name)
125
- .then((decryptedName) => {
126
- reportRequest.name = decryptedName;
127
- })
128
- .catch((reason) => {
129
- ctx.webex.logger.error(
130
- `Error decrypting report name for report ${object.body.id}: ${reason}`
131
- );
132
- });
133
- }
134
-
135
- let reportDescriptionPromise;
136
-
137
- if (reportRequest.description) {
138
- reportDescriptionPromise = ctx.webex.internal.encryption
139
- .decryptText(reportRequest.encryptionKeyUrl, reportRequest.description)
140
- .then((decryptedDescription) => {
141
- reportRequest.description = decryptedDescription;
142
- })
143
- .catch((reason) => {
144
- ctx.webex.logger.error(
145
- `Error decrypting description for report ${object.body.id}: ${reason}`
146
- );
147
- });
148
- }
149
-
150
- let spaceNamePromises = [];
151
-
152
- if (reportRequest.spaceNames) {
153
- spaceNamePromises = Promise.all(
154
- reportRequest.spaceNames.map((spaceName) =>
155
- ctx.webex.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, spaceName)
156
- )
157
- )
158
- .then((decryptedSpaceNames) => {
159
- reportRequest.spaceNames = decryptedSpaceNames;
160
- })
161
- .catch((reason) => {
162
- ctx.webex.logger.error(
163
- `Error decrypting space name for report ${object.body.id}: ${reason}`
164
- );
165
- });
166
- }
167
-
168
- let keywordPromises = [];
169
-
170
- if (reportRequest.keywords) {
171
- keywordPromises = Promise.all(
172
- reportRequest.keywords.map((keyword) =>
173
- ctx.webex.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, keyword)
174
- )
175
- )
176
- .then((decryptedKeywords) => {
177
- reportRequest.keywords = decryptedKeywords;
178
- })
179
- .catch((reason) => {
180
- ctx.webex.logger.error(
181
- `Error decrypting keywords for report ${object.body.id}: ${reason}`
182
- );
183
- });
184
- }
185
-
186
- let emailPromises = [];
187
-
188
- if (reportRequest.emails) {
189
- emailPromises = Promise.all(
190
- reportRequest.emails.map((email) =>
191
- ctx.webex.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, email)
192
- )
193
- )
194
- .then((decryptedEmails) => {
195
- reportRequest.emails = decryptedEmails;
196
- })
197
- .catch((reason) => {
198
- ctx.webex.logger.error(`Error decrypting emails for report ${object.body.id}: ${reason}`);
199
- });
200
- }
201
-
202
- return Promise.all(
203
- [reportNamePromise, reportDescriptionPromise].concat(
204
- spaceNamePromises,
205
- keywordPromises,
206
- emailPromises
207
- )
208
- );
209
- }
210
-
211
- /**
212
- * This function is used to decrypt encrypted properties on the activities that are returned from the eDiscovery Service getContent API
213
- * @param {Object} ctx - An object containing a webex instance and a transform
214
- * @param {Object} object - Generic object that you want to decrypt some property on based on the type
215
- * @param {String} reportId - Id of the report for which content is being retrieved
216
- * @returns {Promise} - Returns a transform promise
217
- */
218
- static decryptReportContent(ctx, object, reportId) {
219
- if (!object || !object.body || !reportId) {
220
- return Promise.resolve();
221
- }
222
- const activity = object.body;
223
-
224
- const promises = [];
225
-
226
- return ctx.webex.internal.ediscovery
227
- .getContentContainerByContainerId(reportId, activity.targetId)
228
- .then((res) => {
229
- const container = res.body;
230
-
231
- if (!container) {
232
- const reason = `Container ${activity.targetId} not found - unable to decrypt activity ${activity.activityId}`;
233
-
234
- activity.error = reason;
235
- ctx.webex.logger.error(reason);
236
-
237
- return Promise.resolve(object);
238
- }
239
-
240
- // add warning properties to the activity - these will be recorded in the downloader
241
- if (container.warning) {
242
- activity.spaceWarning = container.warning; // Remove this property once all clients are using the content container model
243
- activity.containerWarning = container.warning;
244
- }
245
-
246
- // set space name and participants on activity
247
- if (container.containerName) {
248
- activity.spaceName = container.containerName; // Remove this property once all clients are using the content container model
249
- activity.containerName = container.containerName;
250
- } else if (container.isOneOnOne) {
251
- const displayNames = (container.participants || [])
252
- .concat(container.formerParticipants || [])
253
- .map((p) => p.displayName)
254
- .join(' & ');
255
-
256
- // One to One spaces have no space name, use participant names as 'Subject' instead
257
- activity.spaceName = displayNames; // Remove this property once all clients are using the content container model
258
- activity.containerName = displayNames;
259
- } else {
260
- activity.spaceName = ''; // Remove this property once all clients are using the content container model
261
- activity.containerName = '';
262
- }
263
-
264
- // post and share activities have content which needs to be decrypted
265
- // as do meeting, recording activities, customApp extensions, and space information updates
266
- if (
267
- !['post', 'share'].includes(activity.verb) &&
268
- !activity.meeting &&
269
- !activity.recording &&
270
- !(activity.extension && activity.extension.extensionType === 'customApp') &&
271
- !activity.spaceInfo?.name &&
272
- !activity.spaceInfo?.description &&
273
- !activity.encryptedTextKeyValues
274
- ) {
275
- return Promise.resolve(object);
276
- }
277
-
278
- if (!activity.encryptionKeyUrl) {
279
- // If the encryptionKeyUrl is empty we assume the activity is unencrypted
280
- ctx.webex.logger.info(
281
- `Activity ${activity.activityId} cannot be decrypted due to a missing encryption key url`
282
- );
283
-
284
- return Promise.resolve(object);
285
- }
286
-
287
- // CDR Compliance uses org key and does depend on an onBehalfOfUser
288
- if (activity.encryptedTextKeyValues === undefined && !container.onBehalfOfUser) {
289
- const reason = `No user available with which to decrypt activity ${activity.activityId} in container ${activity.targetId}`;
290
-
291
- ctx.webex.logger.error(reason);
292
- activity.error = reason;
293
-
294
- return Promise.resolve(object);
295
- }
296
-
297
- // Decrypt activity message if present
298
- if (activity.objectDisplayName) {
299
- promises.push(
300
- requestWithRetries(
301
- ctx.webex.internal.encryption,
302
- ctx.webex.internal.encryption.decryptText,
303
- [
304
- activity.encryptionKeyUrl,
305
- activity.objectDisplayName,
306
- {onBehalfOf: container.onBehalfOfUser},
307
- ]
308
- )
309
- .then((decryptedMessage) => {
310
- activity.objectDisplayName = decryptedMessage;
311
- })
312
- .catch((reason) => {
313
- ctx.webex.logger.error(
314
- `Decrypt message error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
315
- );
316
- // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
317
- activity.error = reason;
318
-
319
- return Promise.resolve(object);
320
- })
321
- );
322
- }
323
-
324
- // If the activity is a space information update, decrypt the name and description if present
325
- if (activity.spaceInfo?.name) {
326
- promises.push(
327
- requestWithRetries(
328
- ctx.webex.internal.encryption,
329
- ctx.webex.internal.encryption.decryptText,
330
- [
331
- activity.encryptionKeyUrl,
332
- activity.spaceInfo.name,
333
- {onBehalfOf: container.onBehalfOfUser},
334
- ]
335
- )
336
- .then((decryptedMessage) => {
337
- activity.spaceInfo.name = decryptedMessage;
338
- })
339
- .catch((reason) => {
340
- ctx.webex.logger.error(
341
- `Decrypt activity.spaceInfo.name error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
342
- );
343
- activity.error = reason;
344
-
345
- return Promise.resolve(object);
346
- })
347
- );
348
- }
349
- if (activity.spaceInfo?.description) {
350
- promises.push(
351
- requestWithRetries(
352
- ctx.webex.internal.encryption,
353
- ctx.webex.internal.encryption.decryptText,
354
- [
355
- activity.encryptionKeyUrl,
356
- activity.spaceInfo.description,
357
- {onBehalfOf: container.onBehalfOfUser},
358
- ]
359
- )
360
- .then((decryptedMessage) => {
361
- activity.spaceInfo.description = decryptedMessage;
362
- })
363
- .catch((reason) => {
364
- ctx.webex.logger.error(
365
- `Decrypt activity.spaceInfo.description error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
366
- );
367
- activity.error = reason;
368
-
369
- return Promise.resolve(object);
370
- })
371
- );
372
- }
373
- if (activity.spaceInfo?.previousName && activity.spaceInfo.previousEncryptionKeyUrl) {
374
- promises.push(
375
- requestWithRetries(
376
- ctx.webex.internal.encryption,
377
- ctx.webex.internal.encryption.decryptText,
378
- [
379
- activity.spaceInfo.previousEncryptionKeyUrl,
380
- activity.spaceInfo.previousName,
381
- {onBehalfOf: container.onBehalfOfUser},
382
- ]
383
- )
384
- .then((decryptedMessage) => {
385
- activity.spaceInfo.previousName = decryptedMessage;
386
- })
387
- .catch((reason) => {
388
- ctx.webex.logger.error(
389
- `Decrypt activity.spaceInfo.previousName error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
390
- );
391
- activity.error = reason;
392
-
393
- return Promise.resolve(object);
394
- })
395
- );
396
- }
397
-
398
- // Decrypt content url and display name if extension is present
399
- if (
400
- activity.extension &&
401
- activity.extension.objectType === 'extension' &&
402
- activity.extension.extensionType === 'customApp'
403
- ) {
404
- promises.push(
405
- requestWithRetries(
406
- ctx.webex.internal.encryption,
407
- ctx.webex.internal.encryption.decryptText,
408
- [
409
- activity.encryptionKeyUrl,
410
- activity.extension.contentUrl,
411
- {onBehalfOf: container.onBehalfOfUser},
412
- ]
413
- )
414
- .then((decryptedContentUrl) => {
415
- activity.extension.contentUrl = decryptedContentUrl;
416
- })
417
- .catch((reason) => {
418
- ctx.webex.logger.error(
419
- `Decrypt activity.extension.contentUrl error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
420
- );
421
- activity.error = reason;
422
-
423
- return Promise.resolve(object);
424
- })
425
- );
426
-
427
- promises.push(
428
- requestWithRetries(
429
- ctx.webex.internal.encryption,
430
- ctx.webex.internal.encryption.decryptText,
431
- [
432
- activity.encryptionKeyUrl,
433
- activity.extension.displayName,
434
- {onBehalfOf: container.onBehalfOfUser},
435
- ]
436
- )
437
- .then((decryptedDisplayName) => {
438
- activity.extension.displayName = decryptedDisplayName;
439
- })
440
- .catch((reason) => {
441
- ctx.webex.logger.error(
442
- `Decrypt activity.extension.displayName error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
443
- );
444
- activity.error = reason;
445
-
446
- return Promise.resolve(object);
447
- })
448
- );
449
-
450
- // Decrypt webUrl.
451
- if (activity.extension.webUrl) {
452
- promises.push(
453
- requestWithRetries(
454
- ctx.webex.internal.encryption,
455
- ctx.webex.internal.encryption.decryptText,
456
- [
457
- activity.encryptionKeyUrl,
458
- activity.extension.webUrl,
459
- {onBehalfOf: container.onBehalfOfUser},
460
- ]
461
- )
462
- .then((decryptedWebUrl) => {
463
- activity.extension.webUrl = decryptedWebUrl;
464
- })
465
- .catch((reason) => {
466
- ctx.webex.logger.error(
467
- `Decrypt activity.extension.webUrl error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
468
- );
469
- activity.error = reason;
470
-
471
- return Promise.resolve(object);
472
- })
473
- );
474
- }
475
- if (activity.verb === 'update' && activity.extension.previous) {
476
- if (activity.extension.previous.contentUrl) {
477
- promises.push(
478
- requestWithRetries(
479
- ctx.webex.internal.encryption,
480
- ctx.webex.internal.encryption.decryptText,
481
- [
482
- activity.encryptionKeyUrl,
483
- activity.extension.previous.contentUrl,
484
- {onBehalfOf: container.onBehalfOfUser},
485
- ]
486
- )
487
- .then((decryptedPreviousContentUrl) => {
488
- activity.extension.previous.contentUrl = decryptedPreviousContentUrl;
489
- })
490
- .catch((reason) => {
491
- ctx.webex.logger.error(
492
- `Decrypt activity.extension.previous.contentUrl error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
493
- );
494
- activity.error = reason;
495
-
496
- return Promise.resolve(object);
497
- })
498
- );
499
- }
500
- if (activity.extension.previous.displayName) {
501
- promises.push(
502
- requestWithRetries(
503
- ctx.webex.internal.encryption,
504
- ctx.webex.internal.encryption.decryptText,
505
- [
506
- activity.encryptionKeyUrl,
507
- activity.extension.previous.displayName,
508
- {onBehalfOf: container.onBehalfOfUser},
509
- ]
510
- )
511
- .then((decryptedPreviousDisplayName) => {
512
- activity.extension.previous.displayName = decryptedPreviousDisplayName;
513
- })
514
- .catch((reason) => {
515
- ctx.webex.logger.error(
516
- `Decrypt activity.extension.previous.displayName error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
517
- );
518
- activity.error = reason;
519
-
520
- return Promise.resolve(object);
521
- })
522
- );
523
- }
524
- }
525
- }
526
-
527
- // Decrypt encrypted text map if present
528
- if (activity.encryptedTextKeyValues !== undefined) {
529
- for (const [key, value] of Object.entries(activity.encryptedTextKeyValues)) {
530
- promises.push(
531
- requestWithRetries(
532
- ctx.webex.internal.encryption,
533
- ctx.webex.internal.encryption.decryptText,
534
- [activity.encryptionKeyUrl, value]
535
- )
536
- .then((decryptedMessage) => {
537
- activity.encryptedTextKeyValues[key] = decryptedMessage;
538
- })
539
- .catch((reason) => {
540
- ctx.webex.logger.error(
541
- `Decrypt activity.encryptedTextKeyValues error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
542
- );
543
- activity.error = reason;
544
-
545
- return Promise.resolve(object);
546
- })
547
- );
548
- }
549
- }
550
-
551
- // Decrypt meeting title if present
552
- if (activity?.meeting?.title) {
553
- promises.push(
554
- requestWithRetries(
555
- ctx.webex.internal.encryption,
556
- ctx.webex.internal.encryption.decryptText,
557
- [
558
- activity.encryptionKeyUrl,
559
- activity.meeting.title,
560
- {onBehalfOf: container.onBehalfOfUser},
561
- ]
562
- )
563
- .then((decryptedMessage) => {
564
- activity.meeting.title = decryptedMessage;
565
- })
566
- .catch((reason) => {
567
- ctx.webex.logger.error(
568
- `Decrypt activity.meeting.title error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
569
- );
570
- activity.error = reason;
571
-
572
- return Promise.resolve(object);
573
- })
574
- );
575
- }
576
-
577
- // Decrypt meeting recording topic if present
578
- if (activity?.recording?.topic) {
579
- promises.push(
580
- requestWithRetries(
581
- ctx.webex.internal.encryption,
582
- ctx.webex.internal.encryption.decryptText,
583
- [
584
- activity.encryptionKeyUrl,
585
- activity.recording.topic,
586
- {onBehalfOf: container.onBehalfOfUser},
587
- ]
588
- )
589
- .then((decryptedMessage) => {
590
- activity.recording.topic = decryptedMessage;
591
- })
592
- .catch((reason) => {
593
- ctx.webex.logger.error(
594
- `Decrypt activity.recording.topic error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
595
- );
596
- activity.error = reason;
597
-
598
- return Promise.resolve(object);
599
- })
600
- );
601
- }
602
-
603
- // Decrypt shares (files, whiteboards, shared links)
604
- // Array.prototype.concat.apply ignores undefined
605
- let shares = Array.prototype.concat.apply([], activity.files);
606
-
607
- shares = Array.prototype.concat.apply(shares, activity.whiteboards);
608
- shares = Array.prototype.concat.apply(shares, activity.links);
609
- for (let i = 0; i < shares.length; i += 1) {
610
- const share = shares[i];
611
-
612
- // Decrypt the share's display name
613
- // Ignore display names for whiteboards which are unencrypted
614
- if (
615
- share.displayName &&
616
- (!activity.whiteboards || !activity.whiteboards.includes(share))
617
- ) {
618
- promises.push(
619
- requestWithRetries(
620
- ctx.webex.internal.encryption,
621
- ctx.webex.internal.encryption.decryptText,
622
- [
623
- activity.encryptionKeyUrl,
624
- share.displayName,
625
- {onBehalfOf: container.onBehalfOfUser},
626
- ]
627
- )
628
- .then((decryptedDisplayName) => {
629
- share.displayName = decryptedDisplayName;
630
- })
631
- .catch((reason) => {
632
- ctx.webex.logger.warn(
633
- `Decrypt DisplayName error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
634
- );
635
- // add warning property to activity - this will present an indication that there was data loss on the downloader
636
- activity.warning = reason;
637
- })
638
- );
639
- }
640
-
641
- // Shared Links can have additional decryption fields
642
- if (share.microsoftSharedLinkInfo) {
643
- if (share.microsoftSharedLinkInfo.driveId) {
644
- promises.push(
645
- requestWithRetries(
646
- ctx.webex.internal.encryption,
647
- ctx.webex.internal.encryption.decryptText,
648
- [
649
- activity.encryptionKeyUrl,
650
- share.microsoftSharedLinkInfo.driveId,
651
- {onBehalfOf: container.onBehalfOfUser},
652
- ]
653
- )
654
- .then((decryptedDriveId) => {
655
- share.microsoftSharedLinkInfo.driveId = decryptedDriveId;
656
- })
657
- .catch((reason) => {
658
- ctx.webex.logger.error(
659
- `Decrypt share.microsoftSharedLinkInfo.driveId error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
660
- );
661
- // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
662
- activity.error = reason;
663
-
664
- return Promise.resolve(object);
665
- })
666
- );
667
- }
668
-
669
- if (share.microsoftSharedLinkInfo.itemId) {
670
- promises.push(
671
- requestWithRetries(
672
- ctx.webex.internal.encryption,
673
- ctx.webex.internal.encryption.decryptText,
674
- [
675
- activity.encryptionKeyUrl,
676
- share.microsoftSharedLinkInfo.itemId,
677
- {onBehalfOf: container.onBehalfOfUser},
678
- ]
679
- )
680
- .then((decryptedItemId) => {
681
- share.microsoftSharedLinkInfo.itemId = decryptedItemId;
682
- })
683
- .catch((reason) => {
684
- ctx.webex.logger.error(
685
- `Decrypt share.microsoftSharedLinkInfo.itemId error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
686
- );
687
- // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
688
- activity.error = reason;
689
-
690
- return Promise.resolve(object);
691
- })
692
- );
693
- }
694
- }
695
-
696
- // Decrypt the scr (Secure Content Reference) or sslr (Secure Shared Link Reference)
697
- // Unlike a scr the sslr contains only a loc. But decryptScr(...) is flexible and
698
- // leaves the tag, auth, IV, etc fields on the SCR object as undefined.
699
- if (share.scr || share.sslr) {
700
- promises.push(
701
- requestWithRetries(
702
- ctx.webex.internal.encryption,
703
- ctx.webex.internal.encryption.decryptScr,
704
- // A share will have an encryptionKeyUrl when it's activity uses a different encryptionKeyUrl. This can happen when old activities are edited
705
- // and key rotation is turn on.
706
- [
707
- share.encryptionKeyUrl || activity.encryptionKeyUrl,
708
- share.scr || share.sslr,
709
- {onBehalfOf: container.onBehalfOfUser},
710
- ]
711
- )
712
- .then((decryptedSCR) => {
713
- if (share.scr) {
714
- share.scr = decryptedSCR;
715
- } else {
716
- share.sslr = decryptedSCR.loc;
717
- }
718
- })
719
- .catch((reason) => {
720
- ctx.webex.logger.error(
721
- `Decrypt file scr or sslr error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
722
- );
723
- // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
724
- activity.error = reason;
725
-
726
- return Promise.resolve(object);
727
- })
728
- );
729
- }
730
- }
731
-
732
- return Promise.all(promises);
733
- })
734
- .catch((reason) => {
735
- ctx.webex.logger.error(
736
- `Error retrieving content container for: ${activity.activityId} in container ${activity.targetId}: ${reason}`
737
- );
738
- // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
739
- activity.error = reason;
740
-
741
- return Promise.resolve(object);
742
- });
743
- }
744
-
745
- /**
746
- * This function is used to decrypt encrypted properties on the containers that are returned from the eDiscovery Service getContentContainer API
747
- * @param {Object} ctx - An object containing a webex instance and a transform
748
- * @param {Object} object - Generic object that you want to decrypt some property on based on the type
749
- * @returns {Promise} - Returns a transform promise
750
- */
751
- static decryptReportContentContainer(ctx, object) {
752
- if (!object || !object.body) {
753
- return Promise.resolve();
754
- }
755
- const container = object.body;
756
-
757
- if (!container.containerName) {
758
- return Promise.resolve(object);
759
- }
760
-
761
- if (!container.encryptionKeyUrl) {
762
- // If the encryptionKeyUrl is empty we assume the container name is unencrypted
763
- ctx.webex.logger.info(
764
- `${container.containerType} container ${container.containerId} cannot be decrypted due to a missing encryption key url`
765
- );
766
-
767
- return Promise.resolve(object);
768
- }
769
-
770
- if (!container.onBehalfOfUser) {
771
- const reason = `No user available with which to decrypt ${container.containerType} container ${container.containerId}`;
772
-
773
- ctx.webex.logger.error(reason);
774
- container.error = reason;
775
-
776
- return Promise.resolve(object);
777
- }
778
-
779
- // decrypt description if present with a descriptionEncryptionKeyUrl
780
- if (container.description && container.descriptionEncryptionKeyUrl) {
781
- requestWithRetries(ctx.webex.internal.encryption, ctx.webex.internal.encryption.decryptText, [
782
- container.descriptionEncryptionKeyUrl,
783
- container.description,
784
- {onBehalfOf: container.onBehalfOfUser},
785
- ])
786
- .then((decryptedContainerDescription) => {
787
- container.description = decryptedContainerDescription;
788
- })
789
- .catch((reason) => {
790
- ctx.webex.logger.error(
791
- `Decrypt container description error for ${container.containerType} container ${container.containerId}: ${reason}`
792
- );
793
- // add warn property to container info - this warning will be recorded in the downloader
794
- container.warning = reason;
795
- // don't return, attempt to decrypt the name first
796
- });
797
- }
798
-
799
- return requestWithRetries(
800
- ctx.webex.internal.encryption,
801
- ctx.webex.internal.encryption.decryptText,
802
- [container.encryptionKeyUrl, container.containerName, {onBehalfOf: container.onBehalfOfUser}]
803
- )
804
- .then((decryptedContainerName) => {
805
- container.containerName = decryptedContainerName;
806
- })
807
- .catch((reason) => {
808
- ctx.webex.logger.error(
809
- `Decrypt container name error for ${container.containerType} container ${container.containerId}: ${reason}`
810
- );
811
- // add warn property to container info - this warning will be recorded in the downloader
812
- container.warning = reason;
813
-
814
- return Promise.resolve(object);
815
- });
816
- }
817
- }
818
-
819
- export default Transforms;
1
+ import {requestWithRetries} from './retry';
2
+
3
+ /**
4
+ * This class is used to encrypt/decrypt various properties on ReportRequests, Activities and Spaces as they are sent/returned to/from the eDiscovery Service
5
+ */
6
+ class Transforms {
7
+ /**
8
+ * This function is used to encrypt sensitive properties on the ReportRequest before it is sent to the eDiscovery Service createReport API
9
+ * @param {Object} ctx - An object containing a webex instance and a transform
10
+ * @param {Object} object - Generic object that you want to encrypt some property on based on the type
11
+ * @returns {Promise} - Returns a transform promise
12
+ */
13
+ static encryptReportRequest(ctx, object) {
14
+ if (!object || !object.body) {
15
+ return Promise.resolve(object);
16
+ }
17
+ const reportRequest = object.body;
18
+
19
+ return ctx.webex.internal.encryption.kms
20
+ .createUnboundKeys({count: 1})
21
+ .then((keys) => {
22
+ if (keys && keys.length > 0 && keys[0]) {
23
+ reportRequest.encryptionKeyUrl = keys[0].uri;
24
+
25
+ return ctx.webex.internal.encryption.kms
26
+ .createResource({userIds: [keys[0].userId], keys})
27
+ .then(() => {
28
+ const promises = [];
29
+
30
+ if (reportRequest.name) {
31
+ promises.push(
32
+ ctx.webex.internal.encryption
33
+ .encryptText(keys[0], reportRequest.name)
34
+ .then((encryptedName) => {
35
+ reportRequest.name = encryptedName;
36
+ })
37
+ );
38
+ }
39
+
40
+ if (reportRequest.description) {
41
+ promises.push(
42
+ ctx.webex.internal.encryption
43
+ .encryptText(keys[0], reportRequest.description)
44
+ .then((encryptedDescription) => {
45
+ reportRequest.description = encryptedDescription;
46
+ })
47
+ );
48
+ }
49
+
50
+ if (reportRequest.spaceNames) {
51
+ promises.push(
52
+ Promise.all(
53
+ reportRequest.spaceNames.map((spaceName) =>
54
+ ctx.webex.internal.encryption.encryptText(keys[0], spaceName)
55
+ )
56
+ ).then((encryptedSpaceNames) => {
57
+ reportRequest.spaceNames = encryptedSpaceNames;
58
+ })
59
+ );
60
+ }
61
+
62
+ if (reportRequest.keywords) {
63
+ promises.push(
64
+ Promise.all(
65
+ reportRequest.keywords.map((keyword) =>
66
+ ctx.webex.internal.encryption.encryptText(keys[0], keyword)
67
+ )
68
+ ).then((encryptedKeywords) => {
69
+ reportRequest.keywords = encryptedKeywords;
70
+ })
71
+ );
72
+ }
73
+
74
+ if (reportRequest.emails) {
75
+ // store unencrypted emails for ediscovery service to convert to user ids
76
+ reportRequest.unencryptedEmails = reportRequest.emails;
77
+ promises.push(
78
+ Promise.all(
79
+ reportRequest.emails.map((email) =>
80
+ ctx.webex.internal.encryption.encryptText(keys[0], email)
81
+ )
82
+ ).then((encryptedEmails) => {
83
+ reportRequest.emails = encryptedEmails;
84
+ })
85
+ );
86
+ }
87
+
88
+ return Promise.all(promises);
89
+ });
90
+ }
91
+
92
+ return Promise.resolve(object);
93
+ })
94
+ .catch((reason) => {
95
+ ctx.webex.logger.error(
96
+ `Error while encrypting report request: ${reportRequest} : ${reason}`
97
+ );
98
+
99
+ return Promise.reject(reason);
100
+ });
101
+ }
102
+
103
+ /**
104
+ * This function is used to decrypt encrypted properties on the ReportRequest that is returned from the eDiscovery Service getReport(s) API
105
+ * @param {Object} ctx - An object containing a webex instance and a transform
106
+ * @param {Object} object - Generic object that you want to decrypt some property on based on the type
107
+ * @returns {Promise} - Returns a transform promise
108
+ */
109
+ static decryptReportRequest(ctx, object) {
110
+ if (
111
+ !object ||
112
+ !object.body ||
113
+ !object.body.reportRequest ||
114
+ !object.body.reportRequest.encryptionKeyUrl
115
+ ) {
116
+ return Promise.resolve(object);
117
+ }
118
+ const {reportRequest} = object.body;
119
+
120
+ let reportNamePromise;
121
+
122
+ if (reportRequest.name) {
123
+ reportNamePromise = ctx.webex.internal.encryption
124
+ .decryptText(reportRequest.encryptionKeyUrl, reportRequest.name)
125
+ .then((decryptedName) => {
126
+ reportRequest.name = decryptedName;
127
+ })
128
+ .catch((reason) => {
129
+ ctx.webex.logger.error(
130
+ `Error decrypting report name for report ${object.body.id}: ${reason}`
131
+ );
132
+ });
133
+ }
134
+
135
+ let reportDescriptionPromise;
136
+
137
+ if (reportRequest.description) {
138
+ reportDescriptionPromise = ctx.webex.internal.encryption
139
+ .decryptText(reportRequest.encryptionKeyUrl, reportRequest.description)
140
+ .then((decryptedDescription) => {
141
+ reportRequest.description = decryptedDescription;
142
+ })
143
+ .catch((reason) => {
144
+ ctx.webex.logger.error(
145
+ `Error decrypting description for report ${object.body.id}: ${reason}`
146
+ );
147
+ });
148
+ }
149
+
150
+ let spaceNamePromises = [];
151
+
152
+ if (reportRequest.spaceNames) {
153
+ spaceNamePromises = Promise.all(
154
+ reportRequest.spaceNames.map((spaceName) =>
155
+ ctx.webex.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, spaceName)
156
+ )
157
+ )
158
+ .then((decryptedSpaceNames) => {
159
+ reportRequest.spaceNames = decryptedSpaceNames;
160
+ })
161
+ .catch((reason) => {
162
+ ctx.webex.logger.error(
163
+ `Error decrypting space name for report ${object.body.id}: ${reason}`
164
+ );
165
+ });
166
+ }
167
+
168
+ let keywordPromises = [];
169
+
170
+ if (reportRequest.keywords) {
171
+ keywordPromises = Promise.all(
172
+ reportRequest.keywords.map((keyword) =>
173
+ ctx.webex.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, keyword)
174
+ )
175
+ )
176
+ .then((decryptedKeywords) => {
177
+ reportRequest.keywords = decryptedKeywords;
178
+ })
179
+ .catch((reason) => {
180
+ ctx.webex.logger.error(
181
+ `Error decrypting keywords for report ${object.body.id}: ${reason}`
182
+ );
183
+ });
184
+ }
185
+
186
+ let emailPromises = [];
187
+
188
+ if (reportRequest.emails) {
189
+ emailPromises = Promise.all(
190
+ reportRequest.emails.map((email) =>
191
+ ctx.webex.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, email)
192
+ )
193
+ )
194
+ .then((decryptedEmails) => {
195
+ reportRequest.emails = decryptedEmails;
196
+ })
197
+ .catch((reason) => {
198
+ ctx.webex.logger.error(`Error decrypting emails for report ${object.body.id}: ${reason}`);
199
+ });
200
+ }
201
+
202
+ return Promise.all(
203
+ [reportNamePromise, reportDescriptionPromise].concat(
204
+ spaceNamePromises,
205
+ keywordPromises,
206
+ emailPromises
207
+ )
208
+ );
209
+ }
210
+
211
+ /**
212
+ * This function is used to decrypt encrypted properties on the activities that are returned from the eDiscovery Service getContent API
213
+ * @param {Object} ctx - An object containing a webex instance and a transform
214
+ * @param {Object} object - Generic object that you want to decrypt some property on based on the type
215
+ * @param {String} reportId - Id of the report for which content is being retrieved
216
+ * @returns {Promise} - Returns a transform promise
217
+ */
218
+ static decryptReportContent(ctx, object, reportId) {
219
+ if (!object || !object.body || !reportId) {
220
+ return Promise.resolve();
221
+ }
222
+ const activity = object.body;
223
+
224
+ const promises = [];
225
+
226
+ return ctx.webex.internal.ediscovery
227
+ .getContentContainerByContainerId(reportId, activity.targetId)
228
+ .then((res) => {
229
+ const container = res.body;
230
+
231
+ if (!container) {
232
+ const reason = `Container ${activity.targetId} not found - unable to decrypt activity ${activity.activityId}`;
233
+
234
+ activity.error = reason;
235
+ ctx.webex.logger.error(reason);
236
+
237
+ return Promise.resolve(object);
238
+ }
239
+
240
+ // add warning properties to the activity - these will be recorded in the downloader
241
+ if (container.warning) {
242
+ activity.spaceWarning = container.warning; // Remove this property once all clients are using the content container model
243
+ activity.containerWarning = container.warning;
244
+ }
245
+
246
+ // set space name and participants on activity
247
+ if (container.containerName) {
248
+ activity.spaceName = container.containerName; // Remove this property once all clients are using the content container model
249
+ activity.containerName = container.containerName;
250
+ } else if (container.isOneOnOne) {
251
+ const displayNames = (container.participants || [])
252
+ .concat(container.formerParticipants || [])
253
+ .map((p) => p.displayName)
254
+ .join(' & ');
255
+
256
+ // One to One spaces have no space name, use participant names as 'Subject' instead
257
+ activity.spaceName = displayNames; // Remove this property once all clients are using the content container model
258
+ activity.containerName = displayNames;
259
+ } else {
260
+ activity.spaceName = ''; // Remove this property once all clients are using the content container model
261
+ activity.containerName = '';
262
+ }
263
+
264
+ // post and share activities have content which needs to be decrypted
265
+ // as do meeting, recording activities, customApp extensions, and space information updates
266
+ if (
267
+ !['post', 'share'].includes(activity.verb) &&
268
+ !activity.meeting &&
269
+ !activity.recording &&
270
+ !(activity.extension && activity.extension.extensionType === 'customApp') &&
271
+ !activity.spaceInfo?.name &&
272
+ !activity.spaceInfo?.description &&
273
+ !activity.encryptedTextKeyValues
274
+ ) {
275
+ return Promise.resolve(object);
276
+ }
277
+
278
+ if (!activity.encryptionKeyUrl) {
279
+ // If the encryptionKeyUrl is empty we assume the activity is unencrypted
280
+ ctx.webex.logger.info(
281
+ `Activity ${activity.activityId} cannot be decrypted due to a missing encryption key url`
282
+ );
283
+
284
+ return Promise.resolve(object);
285
+ }
286
+
287
+ // CDR Compliance uses org key and does depend on an onBehalfOfUser
288
+ if (activity.encryptedTextKeyValues === undefined && !container.onBehalfOfUser) {
289
+ const reason = `No user available with which to decrypt activity ${activity.activityId} in container ${activity.targetId}`;
290
+
291
+ ctx.webex.logger.error(reason);
292
+ activity.error = reason;
293
+
294
+ return Promise.resolve(object);
295
+ }
296
+
297
+ // Decrypt activity message if present
298
+ if (activity.objectDisplayName) {
299
+ promises.push(
300
+ requestWithRetries(
301
+ ctx.webex.internal.encryption,
302
+ ctx.webex.internal.encryption.decryptText,
303
+ [
304
+ activity.encryptionKeyUrl,
305
+ activity.objectDisplayName,
306
+ {onBehalfOf: container.onBehalfOfUser},
307
+ ]
308
+ )
309
+ .then((decryptedMessage) => {
310
+ activity.objectDisplayName = decryptedMessage;
311
+ })
312
+ .catch((reason) => {
313
+ ctx.webex.logger.error(
314
+ `Decrypt message error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
315
+ );
316
+ // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
317
+ activity.error = reason;
318
+
319
+ return Promise.resolve(object);
320
+ })
321
+ );
322
+ }
323
+
324
+ // If the activity is a space information update, decrypt the name and description if present
325
+ if (activity.spaceInfo?.name) {
326
+ promises.push(
327
+ requestWithRetries(
328
+ ctx.webex.internal.encryption,
329
+ ctx.webex.internal.encryption.decryptText,
330
+ [
331
+ activity.encryptionKeyUrl,
332
+ activity.spaceInfo.name,
333
+ {onBehalfOf: container.onBehalfOfUser},
334
+ ]
335
+ )
336
+ .then((decryptedMessage) => {
337
+ activity.spaceInfo.name = decryptedMessage;
338
+ })
339
+ .catch((reason) => {
340
+ ctx.webex.logger.error(
341
+ `Decrypt activity.spaceInfo.name error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
342
+ );
343
+ activity.error = reason;
344
+
345
+ return Promise.resolve(object);
346
+ })
347
+ );
348
+ }
349
+ if (activity.spaceInfo?.description) {
350
+ promises.push(
351
+ requestWithRetries(
352
+ ctx.webex.internal.encryption,
353
+ ctx.webex.internal.encryption.decryptText,
354
+ [
355
+ activity.encryptionKeyUrl,
356
+ activity.spaceInfo.description,
357
+ {onBehalfOf: container.onBehalfOfUser},
358
+ ]
359
+ )
360
+ .then((decryptedMessage) => {
361
+ activity.spaceInfo.description = decryptedMessage;
362
+ })
363
+ .catch((reason) => {
364
+ ctx.webex.logger.error(
365
+ `Decrypt activity.spaceInfo.description error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
366
+ );
367
+ activity.error = reason;
368
+
369
+ return Promise.resolve(object);
370
+ })
371
+ );
372
+ }
373
+ if (activity.spaceInfo?.previousName && activity.spaceInfo.previousEncryptionKeyUrl) {
374
+ promises.push(
375
+ requestWithRetries(
376
+ ctx.webex.internal.encryption,
377
+ ctx.webex.internal.encryption.decryptText,
378
+ [
379
+ activity.spaceInfo.previousEncryptionKeyUrl,
380
+ activity.spaceInfo.previousName,
381
+ {onBehalfOf: container.onBehalfOfUser},
382
+ ]
383
+ )
384
+ .then((decryptedMessage) => {
385
+ activity.spaceInfo.previousName = decryptedMessage;
386
+ })
387
+ .catch((reason) => {
388
+ ctx.webex.logger.error(
389
+ `Decrypt activity.spaceInfo.previousName error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
390
+ );
391
+ activity.error = reason;
392
+
393
+ return Promise.resolve(object);
394
+ })
395
+ );
396
+ }
397
+
398
+ // Decrypt content url and display name if extension is present
399
+ if (
400
+ activity.extension &&
401
+ activity.extension.objectType === 'extension' &&
402
+ activity.extension.extensionType === 'customApp'
403
+ ) {
404
+ promises.push(
405
+ requestWithRetries(
406
+ ctx.webex.internal.encryption,
407
+ ctx.webex.internal.encryption.decryptText,
408
+ [
409
+ activity.encryptionKeyUrl,
410
+ activity.extension.contentUrl,
411
+ {onBehalfOf: container.onBehalfOfUser},
412
+ ]
413
+ )
414
+ .then((decryptedContentUrl) => {
415
+ activity.extension.contentUrl = decryptedContentUrl;
416
+ })
417
+ .catch((reason) => {
418
+ ctx.webex.logger.error(
419
+ `Decrypt activity.extension.contentUrl error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
420
+ );
421
+ activity.error = reason;
422
+
423
+ return Promise.resolve(object);
424
+ })
425
+ );
426
+
427
+ promises.push(
428
+ requestWithRetries(
429
+ ctx.webex.internal.encryption,
430
+ ctx.webex.internal.encryption.decryptText,
431
+ [
432
+ activity.encryptionKeyUrl,
433
+ activity.extension.displayName,
434
+ {onBehalfOf: container.onBehalfOfUser},
435
+ ]
436
+ )
437
+ .then((decryptedDisplayName) => {
438
+ activity.extension.displayName = decryptedDisplayName;
439
+ })
440
+ .catch((reason) => {
441
+ ctx.webex.logger.error(
442
+ `Decrypt activity.extension.displayName error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
443
+ );
444
+ activity.error = reason;
445
+
446
+ return Promise.resolve(object);
447
+ })
448
+ );
449
+
450
+ // Decrypt webUrl.
451
+ if (activity.extension.webUrl) {
452
+ promises.push(
453
+ requestWithRetries(
454
+ ctx.webex.internal.encryption,
455
+ ctx.webex.internal.encryption.decryptText,
456
+ [
457
+ activity.encryptionKeyUrl,
458
+ activity.extension.webUrl,
459
+ {onBehalfOf: container.onBehalfOfUser},
460
+ ]
461
+ )
462
+ .then((decryptedWebUrl) => {
463
+ activity.extension.webUrl = decryptedWebUrl;
464
+ })
465
+ .catch((reason) => {
466
+ ctx.webex.logger.error(
467
+ `Decrypt activity.extension.webUrl error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
468
+ );
469
+ activity.error = reason;
470
+
471
+ return Promise.resolve(object);
472
+ })
473
+ );
474
+ }
475
+ if (activity.verb === 'update' && activity.extension.previous) {
476
+ if (activity.extension.previous.contentUrl) {
477
+ promises.push(
478
+ requestWithRetries(
479
+ ctx.webex.internal.encryption,
480
+ ctx.webex.internal.encryption.decryptText,
481
+ [
482
+ activity.encryptionKeyUrl,
483
+ activity.extension.previous.contentUrl,
484
+ {onBehalfOf: container.onBehalfOfUser},
485
+ ]
486
+ )
487
+ .then((decryptedPreviousContentUrl) => {
488
+ activity.extension.previous.contentUrl = decryptedPreviousContentUrl;
489
+ })
490
+ .catch((reason) => {
491
+ ctx.webex.logger.error(
492
+ `Decrypt activity.extension.previous.contentUrl error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
493
+ );
494
+ activity.error = reason;
495
+
496
+ return Promise.resolve(object);
497
+ })
498
+ );
499
+ }
500
+ if (activity.extension.previous.displayName) {
501
+ promises.push(
502
+ requestWithRetries(
503
+ ctx.webex.internal.encryption,
504
+ ctx.webex.internal.encryption.decryptText,
505
+ [
506
+ activity.encryptionKeyUrl,
507
+ activity.extension.previous.displayName,
508
+ {onBehalfOf: container.onBehalfOfUser},
509
+ ]
510
+ )
511
+ .then((decryptedPreviousDisplayName) => {
512
+ activity.extension.previous.displayName = decryptedPreviousDisplayName;
513
+ })
514
+ .catch((reason) => {
515
+ ctx.webex.logger.error(
516
+ `Decrypt activity.extension.previous.displayName error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
517
+ );
518
+ activity.error = reason;
519
+
520
+ return Promise.resolve(object);
521
+ })
522
+ );
523
+ }
524
+ }
525
+ }
526
+
527
+ // Decrypt encrypted text map if present
528
+ if (activity.encryptedTextKeyValues !== undefined) {
529
+ for (const [key, value] of Object.entries(activity.encryptedTextKeyValues)) {
530
+ promises.push(
531
+ requestWithRetries(
532
+ ctx.webex.internal.encryption,
533
+ ctx.webex.internal.encryption.decryptText,
534
+ [activity.encryptionKeyUrl, value]
535
+ )
536
+ .then((decryptedMessage) => {
537
+ activity.encryptedTextKeyValues[key] = decryptedMessage;
538
+ })
539
+ .catch((reason) => {
540
+ ctx.webex.logger.error(
541
+ `Decrypt activity.encryptedTextKeyValues error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
542
+ );
543
+ activity.error = reason;
544
+
545
+ return Promise.resolve(object);
546
+ })
547
+ );
548
+ }
549
+ }
550
+
551
+ // Decrypt meeting title if present
552
+ if (activity?.meeting?.title) {
553
+ promises.push(
554
+ requestWithRetries(
555
+ ctx.webex.internal.encryption,
556
+ ctx.webex.internal.encryption.decryptText,
557
+ [
558
+ activity.encryptionKeyUrl,
559
+ activity.meeting.title,
560
+ {onBehalfOf: container.onBehalfOfUser},
561
+ ]
562
+ )
563
+ .then((decryptedMessage) => {
564
+ activity.meeting.title = decryptedMessage;
565
+ })
566
+ .catch((reason) => {
567
+ ctx.webex.logger.error(
568
+ `Decrypt activity.meeting.title error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
569
+ );
570
+ activity.error = reason;
571
+
572
+ return Promise.resolve(object);
573
+ })
574
+ );
575
+ }
576
+
577
+ // Decrypt meeting recording topic if present
578
+ if (activity?.recording?.topic) {
579
+ promises.push(
580
+ requestWithRetries(
581
+ ctx.webex.internal.encryption,
582
+ ctx.webex.internal.encryption.decryptText,
583
+ [
584
+ activity.encryptionKeyUrl,
585
+ activity.recording.topic,
586
+ {onBehalfOf: container.onBehalfOfUser},
587
+ ]
588
+ )
589
+ .then((decryptedMessage) => {
590
+ activity.recording.topic = decryptedMessage;
591
+ })
592
+ .catch((reason) => {
593
+ ctx.webex.logger.error(
594
+ `Decrypt activity.recording.topic error for activity ${activity.activityId} in container ${activity.targetId}: ${reason}`
595
+ );
596
+ activity.error = reason;
597
+
598
+ return Promise.resolve(object);
599
+ })
600
+ );
601
+ }
602
+
603
+ // Decrypt shares (files, whiteboards, shared links)
604
+ // Array.prototype.concat.apply ignores undefined
605
+ let shares = Array.prototype.concat.apply([], activity.files);
606
+
607
+ shares = Array.prototype.concat.apply(shares, activity.whiteboards);
608
+ shares = Array.prototype.concat.apply(shares, activity.links);
609
+ for (let i = 0; i < shares.length; i += 1) {
610
+ const share = shares[i];
611
+
612
+ // Decrypt the share's display name
613
+ // Ignore display names for whiteboards which are unencrypted
614
+ if (
615
+ share.displayName &&
616
+ (!activity.whiteboards || !activity.whiteboards.includes(share))
617
+ ) {
618
+ promises.push(
619
+ requestWithRetries(
620
+ ctx.webex.internal.encryption,
621
+ ctx.webex.internal.encryption.decryptText,
622
+ [
623
+ activity.encryptionKeyUrl,
624
+ share.displayName,
625
+ {onBehalfOf: container.onBehalfOfUser},
626
+ ]
627
+ )
628
+ .then((decryptedDisplayName) => {
629
+ share.displayName = decryptedDisplayName;
630
+ })
631
+ .catch((reason) => {
632
+ ctx.webex.logger.warn(
633
+ `Decrypt DisplayName error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
634
+ );
635
+ // add warning property to activity - this will present an indication that there was data loss on the downloader
636
+ activity.warning = reason;
637
+ })
638
+ );
639
+ }
640
+
641
+ // Shared Links can have additional decryption fields
642
+ if (share.microsoftSharedLinkInfo) {
643
+ if (share.microsoftSharedLinkInfo.driveId) {
644
+ promises.push(
645
+ requestWithRetries(
646
+ ctx.webex.internal.encryption,
647
+ ctx.webex.internal.encryption.decryptText,
648
+ [
649
+ activity.encryptionKeyUrl,
650
+ share.microsoftSharedLinkInfo.driveId,
651
+ {onBehalfOf: container.onBehalfOfUser},
652
+ ]
653
+ )
654
+ .then((decryptedDriveId) => {
655
+ share.microsoftSharedLinkInfo.driveId = decryptedDriveId;
656
+ })
657
+ .catch((reason) => {
658
+ ctx.webex.logger.error(
659
+ `Decrypt share.microsoftSharedLinkInfo.driveId error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
660
+ );
661
+ // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
662
+ activity.error = reason;
663
+
664
+ return Promise.resolve(object);
665
+ })
666
+ );
667
+ }
668
+
669
+ if (share.microsoftSharedLinkInfo.itemId) {
670
+ promises.push(
671
+ requestWithRetries(
672
+ ctx.webex.internal.encryption,
673
+ ctx.webex.internal.encryption.decryptText,
674
+ [
675
+ activity.encryptionKeyUrl,
676
+ share.microsoftSharedLinkInfo.itemId,
677
+ {onBehalfOf: container.onBehalfOfUser},
678
+ ]
679
+ )
680
+ .then((decryptedItemId) => {
681
+ share.microsoftSharedLinkInfo.itemId = decryptedItemId;
682
+ })
683
+ .catch((reason) => {
684
+ ctx.webex.logger.error(
685
+ `Decrypt share.microsoftSharedLinkInfo.itemId error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
686
+ );
687
+ // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
688
+ activity.error = reason;
689
+
690
+ return Promise.resolve(object);
691
+ })
692
+ );
693
+ }
694
+ }
695
+
696
+ // Decrypt the scr (Secure Content Reference) or sslr (Secure Shared Link Reference)
697
+ // Unlike a scr the sslr contains only a loc. But decryptScr(...) is flexible and
698
+ // leaves the tag, auth, IV, etc fields on the SCR object as undefined.
699
+ if (share.scr || share.sslr) {
700
+ promises.push(
701
+ requestWithRetries(
702
+ ctx.webex.internal.encryption,
703
+ ctx.webex.internal.encryption.decryptScr,
704
+ // A share will have an encryptionKeyUrl when it's activity uses a different encryptionKeyUrl. This can happen when old activities are edited
705
+ // and key rotation is turn on.
706
+ [
707
+ share.encryptionKeyUrl || activity.encryptionKeyUrl,
708
+ share.scr || share.sslr,
709
+ {onBehalfOf: container.onBehalfOfUser},
710
+ ]
711
+ )
712
+ .then((decryptedSCR) => {
713
+ if (share.scr) {
714
+ share.scr = decryptedSCR;
715
+ } else {
716
+ share.sslr = decryptedSCR.loc;
717
+ }
718
+ })
719
+ .catch((reason) => {
720
+ ctx.webex.logger.error(
721
+ `Decrypt file scr or sslr error for activity ${activity.activityId} in container ${activity.targetId} for share type: ${share.mimeType}, size: ${share.fileSize}, and url: ${share.url} due to error: ${reason}`
722
+ );
723
+ // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
724
+ activity.error = reason;
725
+
726
+ return Promise.resolve(object);
727
+ })
728
+ );
729
+ }
730
+ }
731
+
732
+ return Promise.all(promises);
733
+ })
734
+ .catch((reason) => {
735
+ ctx.webex.logger.error(
736
+ `Error retrieving content container for: ${activity.activityId} in container ${activity.targetId}: ${reason}`
737
+ );
738
+ // add error property to activity - this error will be recorded in the downloader and the activity omitted from the report
739
+ activity.error = reason;
740
+
741
+ return Promise.resolve(object);
742
+ });
743
+ }
744
+
745
+ /**
746
+ * This function is used to decrypt encrypted properties on the containers that are returned from the eDiscovery Service getContentContainer API
747
+ * @param {Object} ctx - An object containing a webex instance and a transform
748
+ * @param {Object} object - Generic object that you want to decrypt some property on based on the type
749
+ * @returns {Promise} - Returns a transform promise
750
+ */
751
+ static decryptReportContentContainer(ctx, object) {
752
+ if (!object || !object.body) {
753
+ return Promise.resolve();
754
+ }
755
+ const container = object.body;
756
+
757
+ if (!container.containerName) {
758
+ return Promise.resolve(object);
759
+ }
760
+
761
+ if (!container.encryptionKeyUrl) {
762
+ // If the encryptionKeyUrl is empty we assume the container name is unencrypted
763
+ ctx.webex.logger.info(
764
+ `${container.containerType} container ${container.containerId} cannot be decrypted due to a missing encryption key url`
765
+ );
766
+
767
+ return Promise.resolve(object);
768
+ }
769
+
770
+ if (!container.onBehalfOfUser) {
771
+ const reason = `No user available with which to decrypt ${container.containerType} container ${container.containerId}`;
772
+
773
+ ctx.webex.logger.error(reason);
774
+ container.error = reason;
775
+
776
+ return Promise.resolve(object);
777
+ }
778
+
779
+ // decrypt description if present with a descriptionEncryptionKeyUrl
780
+ if (container.description && container.descriptionEncryptionKeyUrl) {
781
+ requestWithRetries(ctx.webex.internal.encryption, ctx.webex.internal.encryption.decryptText, [
782
+ container.descriptionEncryptionKeyUrl,
783
+ container.description,
784
+ {onBehalfOf: container.onBehalfOfUser},
785
+ ])
786
+ .then((decryptedContainerDescription) => {
787
+ container.description = decryptedContainerDescription;
788
+ })
789
+ .catch((reason) => {
790
+ ctx.webex.logger.error(
791
+ `Decrypt container description error for ${container.containerType} container ${container.containerId}: ${reason}`
792
+ );
793
+ // add warn property to container info - this warning will be recorded in the downloader
794
+ container.warning = reason;
795
+ // don't return, attempt to decrypt the name first
796
+ });
797
+ }
798
+
799
+ return requestWithRetries(
800
+ ctx.webex.internal.encryption,
801
+ ctx.webex.internal.encryption.decryptText,
802
+ [container.encryptionKeyUrl, container.containerName, {onBehalfOf: container.onBehalfOfUser}]
803
+ )
804
+ .then((decryptedContainerName) => {
805
+ container.containerName = decryptedContainerName;
806
+ })
807
+ .catch((reason) => {
808
+ ctx.webex.logger.error(
809
+ `Decrypt container name error for ${container.containerType} container ${container.containerId}: ${reason}`
810
+ );
811
+ // add warn property to container info - this warning will be recorded in the downloader
812
+ container.warning = reason;
813
+
814
+ return Promise.resolve(object);
815
+ });
816
+ }
817
+ }
818
+
819
+ export default Transforms;