opal-security 3.1.1-beta.4dbd858 → 3.1.1-beta.4e22fcc

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.
@@ -1,18 +1,33 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createEmptyRequestMetadata = createEmptyRequestMetadata;
3
+ exports.initEmptyRequestMetadata = initEmptyRequestMetadata;
4
4
  exports.selectRequestableItems = selectRequestableItems;
5
- exports.chooseAssets = chooseAssets;
6
- exports.chooseRoles = chooseRoles;
7
5
  exports.doneSelectingAssets = doneSelectingAssets;
8
6
  exports.setRequestDefaults = setRequestDefaults;
9
7
  exports.promptForReason = promptForReason;
10
8
  exports.promptForExpiration = promptForExpiration;
9
+ exports.promptRequestSubmission = promptRequestSubmission;
11
10
  exports.submitFinalRequest = submitFinalRequest;
11
+ exports.bypassRequestSelection = bypassRequestSelection;
12
+ exports.bypassDuration = bypassDuration;
13
+ const chalk_1 = require("chalk");
12
14
  const inquirer = require("inquirer");
13
15
  const graphql_1 = require("../graphql");
14
- inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt"));
15
- function createEmptyRequestMetadata() {
16
+ const graphql_2 = require("../graphql/graphql");
17
+ const displays_1 = require("../utils/displays");
18
+ const config_1 = require("./config");
19
+ const { AutoComplete, Select, prompt, Form } = require("enquirer");
20
+ function entityTypeFromString(str) {
21
+ if (str === "Resource") {
22
+ return graphql_2.EntityType.Resource;
23
+ }
24
+ if (str === "Group") {
25
+ return graphql_2.EntityType.Group;
26
+ }
27
+ // if type unknown, default to resource
28
+ return graphql_2.EntityType.Resource;
29
+ }
30
+ function initEmptyRequestMetadata() {
16
31
  // Initialize with empty defaults
17
32
  const requestDefaults = {
18
33
  durationOptions: [],
@@ -28,6 +43,9 @@ function createEmptyRequestMetadata() {
28
43
  return {
29
44
  requestMap,
30
45
  requestDefaults,
46
+ durationLabel: "",
47
+ durationInMinutes: 0,
48
+ reason: "",
31
49
  };
32
50
  }
33
51
  // Queries and Mutations
@@ -73,18 +91,23 @@ async function queryRequestableApps(cmd, client, input) {
73
91
  });
74
92
  return (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.data) === null || _a === void 0 ? void 0 : _a.appsV2) === null || _b === void 0 ? void 0 : _b.edges.map((edge) => {
75
93
  let type = undefined;
76
- if (edge.node.__typename === "Resource") {
77
- type = edge.node.resourceType;
94
+ switch (edge.node.__typename) {
95
+ case "Resource":
96
+ type = edge.node.resourceType;
97
+ break;
98
+ case "Connection":
99
+ type = edge.node.connectionType;
100
+ break;
101
+ default:
102
+ type = edge.node.__typename;
78
103
  }
79
- if (edge.node.__typename === "Connection") {
80
- type = edge.node.connectionType;
81
- }
82
- const label = `${edge.node.displayName} (${type})`;
83
104
  return {
84
- name: label,
105
+ message: `${edge.node.displayName} [${type}]`,
85
106
  value: {
86
107
  id: edge.node.id,
87
- name: label,
108
+ name: edge.node.displayName,
109
+ type: type,
110
+ toString: () => edge.node.displayName,
88
111
  },
89
112
  };
90
113
  });
@@ -147,16 +170,16 @@ async function queryRequestableAssets(cmd, client, appId, input) {
147
170
  switch (resp.data.app.__typename) {
148
171
  case "App":
149
172
  return (_d = (_c = (_b = (_a = resp.data) === null || _a === void 0 ? void 0 : _a.app) === null || _b === void 0 ? void 0 : _b.items) === null || _c === void 0 ? void 0 : _c.items) === null || _d === void 0 ? void 0 : _d.map((item) => {
150
- var _a, _b, _c, _d, _e, _f;
173
+ var _a, _b, _c, _d, _e, _f, _g, _h;
151
174
  const name = ((_a = item.resource) === null || _a === void 0 ? void 0 : _a.name) || ((_b = item.group) === null || _b === void 0 ? void 0 : _b.name);
152
175
  const id = ((_c = item.resource) === null || _c === void 0 ? void 0 : _c.id) || ((_d = item.group) === null || _d === void 0 ? void 0 : _d.id);
153
176
  const type = ((_e = item.resource) === null || _e === void 0 ? void 0 : _e.__typename) || ((_f = item.group) === null || _f === void 0 ? void 0 : _f.__typename);
154
- const label = `${name} (${type})`;
155
177
  return {
156
- name: label,
178
+ message: `${name} [${type}]`,
157
179
  value: {
158
- name: label,
159
- id: id,
180
+ name: name || "",
181
+ id: id || "",
182
+ type: entityTypeFromString(((_g = item.resource) === null || _g === void 0 ? void 0 : _g.__typename) || ((_h = item.group) === null || _h === void 0 ? void 0 : _h.__typename)),
160
183
  },
161
184
  };
162
185
  });
@@ -182,7 +205,6 @@ const RESOURCE_ROLES_QUERY = (0, graphql_1.graphql)(`
182
205
  __typename
183
206
  ... on ResourceAccessLevelsResult {
184
207
  accessLevels {
185
- __typename
186
208
  ... on ResourceAccessLevel {
187
209
  accessLevelName
188
210
  accessLevelRemoteId
@@ -195,34 +217,82 @@ const RESOURCE_ROLES_QUERY = (0, graphql_1.graphql)(`
195
217
  }
196
218
  }
197
219
  `);
198
- async function queryResourceRoles(cmd, client, resourceId) {
199
- var _a, _b, _c, _d, _e;
220
+ const GROUP_ROLES_QUERY = (0, graphql_1.graphql)(`
221
+ query GroupAccessLevels($groupId: GroupId!) {
222
+ groupAccessLevels(
223
+ input: { groupId: $groupId }
224
+ ) {
225
+ ... on GroupAccessLevelsResult {
226
+ groupId
227
+ accessLevels {
228
+ ... on GroupAccessLevel {
229
+ accessLevelName
230
+ accessLevelRemoteId
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ `);
237
+ async function queryAssetRoles(cmd, client, assetType, assetId) {
238
+ var _a, _b, _c, _d, _e, _f, _g, _h;
200
239
  try {
201
- const resp = await client.query({
202
- query: RESOURCE_ROLES_QUERY,
203
- variables: {
204
- resourceId: resourceId,
205
- },
206
- fetchPolicy: "network-only", // to avoid caching
207
- });
208
- // no fall through doesn't consider process.exit();
209
- let x;
210
- switch (resp.data.accessLevels.__typename) {
211
- case "ResourceAccessLevelsResult":
212
- return (_c = (_b = (_a = resp.data) === null || _a === void 0 ? void 0 : _a.accessLevels) === null || _b === void 0 ? void 0 : _b.accessLevels) === null || _c === void 0 ? void 0 : _c.map((role) => {
213
- return {
214
- name: role.accessLevelName,
215
- value: {
216
- name: role.accessLevelName,
217
- id: role.accessLevelRemoteId,
218
- },
219
- };
240
+ switch (assetType) {
241
+ case "Resource": {
242
+ const resp = await client.query({
243
+ query: RESOURCE_ROLES_QUERY,
244
+ variables: {
245
+ resourceId: assetId,
246
+ },
247
+ fetchPolicy: "network-only", // to avoid caching
220
248
  });
221
- case "ResourceNotFoundError":
222
- x = cmd.error((_e = (_d = resp.data) === null || _d === void 0 ? void 0 : _d.accessLevels) === null || _e === void 0 ? void 0 : _e.message);
223
- break;
224
- default:
225
- cmd.error(resp.error || "Unknown error occurred.");
249
+ // no fall through doesn't consider process.exit();
250
+ let x;
251
+ switch (resp.data.accessLevels.__typename) {
252
+ case "ResourceAccessLevelsResult":
253
+ return (_c = (_b = (_a = resp.data) === null || _a === void 0 ? void 0 : _a.accessLevels) === null || _b === void 0 ? void 0 : _b.accessLevels) === null || _c === void 0 ? void 0 : _c.map((role) => {
254
+ return {
255
+ message: role.accessLevelName || "",
256
+ value: {
257
+ name: role.accessLevelName || "",
258
+ id: role.accessLevelRemoteId || "",
259
+ },
260
+ };
261
+ });
262
+ case "ResourceNotFoundError":
263
+ x = cmd.error((_e = (_d = resp.data) === null || _d === void 0 ? void 0 : _d.accessLevels) === null || _e === void 0 ? void 0 : _e.message);
264
+ break;
265
+ default:
266
+ cmd.error(resp.error || "Unknown error occurred.");
267
+ }
268
+ return;
269
+ }
270
+ case "Group": {
271
+ const resp = await client.query({
272
+ query: GROUP_ROLES_QUERY,
273
+ variables: {
274
+ groupId: assetId,
275
+ },
276
+ fetchPolicy: "network-only", // to avoid caching
277
+ });
278
+ // no fall through doesn't consider process.exit();
279
+ let x;
280
+ switch (resp.data.groupAccessLevels.__typename) {
281
+ case "GroupAccessLevelsResult":
282
+ return (_h = (_g = (_f = resp.data) === null || _f === void 0 ? void 0 : _f.groupAccessLevels) === null || _g === void 0 ? void 0 : _g.accessLevels) === null || _h === void 0 ? void 0 : _h.map((role) => {
283
+ return {
284
+ message: role.accessLevelName,
285
+ value: {
286
+ name: role.accessLevelName,
287
+ id: role.accessLevelRemoteId,
288
+ },
289
+ };
290
+ });
291
+ default:
292
+ x = cmd.error(resp.error || "Unknown error occurred.");
293
+ }
294
+ return;
295
+ }
226
296
  }
227
297
  }
228
298
  catch (error) {
@@ -241,16 +311,18 @@ const REQUEST_DEFAULTS_QUERY = (0, graphql_1.graphql)(`
241
311
  requestedGroups: $requestedGroups,
242
312
  }
243
313
  ) {
244
- durationOptions {
245
- durationInMinutes
246
- label
314
+ ... on RequestDefaults {
315
+ durationOptions {
316
+ durationInMinutes
317
+ label
318
+ }
319
+ recommendedDurationInMinutes
320
+ defaultDurationInMinutes
321
+ maxDurationInMinutes
322
+ requireSupportTicket
323
+ reasonOptional
324
+ requesterIsAdmin
247
325
  }
248
- recommendedDurationInMinutes
249
- defaultDurationInMinutes
250
- maxDurationInMinutes
251
- requireSupportTicket
252
- reasonOptional
253
- requesterIsAdmin
254
326
  }
255
327
  }`);
256
328
  async function queryRequestDefaults(cmd, client, requestedResources, requestedGroups) {
@@ -271,37 +343,406 @@ async function queryRequestDefaults(cmd, client, requestedResources, requestedGr
271
343
  }
272
344
  }
273
345
  }
346
+ const CREATE_REQUEST_MUTATION = (0, graphql_1.graphql)(`
347
+ mutation CreateRequest(
348
+ $requestedResources: [RequestedResourceInput!]!
349
+ $requestedGroups: [RequestedGroupInput!]!
350
+ $reason: String!
351
+ $durationInMinutes: Int
352
+ ) {
353
+ createRequest(
354
+ input: {
355
+ requestedResources: $requestedResources
356
+ requestedGroups: $requestedGroups
357
+ reason: $reason
358
+ durationInMinutes: $durationInMinutes
359
+ }
360
+ ) {
361
+ ... on CreateRequestResult {
362
+ request {
363
+ id
364
+ status
365
+ }
366
+ }
367
+ ... on RequestDurationTooLargeError {
368
+ message
369
+ }
370
+ ... on RequestRequiresUserAuthTokenForConnectionError {
371
+ message
372
+ }
373
+ ... on NoReviewersSetForOwnerError {
374
+ message
375
+ ownerId
376
+ }
377
+ ... on NoReviewersSetForResourceError {
378
+ message
379
+ resourceId
380
+ }
381
+ ... on NoReviewersSetForGroupError {
382
+ message
383
+ groupId
384
+ }
385
+ ... on NoManagerSetForRequestingUserError {
386
+ message
387
+ }
388
+ ... on MfaInvalidError {
389
+ message
390
+ }
391
+ ... on BulkRequestTooLargeError {
392
+ message
393
+ }
394
+ ... on ItemCannotBeRequestedError {
395
+ message
396
+ }
397
+ ... on UserCannotRequestAccessForTargetGroupError {
398
+ message
399
+ groupId
400
+ userId
401
+ }
402
+ ... on GroupNestingNotAllowedError {
403
+ message
404
+ fromGroupId
405
+ toGroupId
406
+ }
407
+ ... on TargetUserHasNestedAccessError {
408
+ message
409
+ groupIds
410
+ }
411
+ ... on RequestReasonMissingError {
412
+ message
413
+ }
414
+ ... on RequestFieldValueMissingError {
415
+ message
416
+ fieldName
417
+ }
418
+ ... on LinkedGroupNotRequestableError {
419
+ message
420
+ sourceGroupId
421
+ groupBindingId
422
+ }
423
+ ... on RequestReasonBelowMinLengthError {
424
+ message
425
+ }
426
+
427
+ }
428
+ }
429
+ `);
430
+ async function createRequest(cmd, client, requestedResources, requestedGroups, reason, durationInMinutes) {
431
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
432
+ try {
433
+ const resp = await client.mutate({
434
+ mutation: CREATE_REQUEST_MUTATION,
435
+ variables: {
436
+ requestedResources: requestedResources,
437
+ requestedGroups: requestedGroups,
438
+ reason: reason,
439
+ durationInMinutes: durationInMinutes,
440
+ },
441
+ });
442
+ switch ((_a = resp.data) === null || _a === void 0 ? void 0 : _a.createRequest.__typename) {
443
+ case "CreateRequestResult":
444
+ return (_b = resp.data) === null || _b === void 0 ? void 0 : _b.createRequest.request;
445
+ case "RequestDurationTooLargeError":
446
+ cmd.log((_c = resp.data) === null || _c === void 0 ? void 0 : _c.createRequest.message);
447
+ break;
448
+ case "RequestRequiresUserAuthTokenForConnectionError":
449
+ cmd.log((_d = resp.data) === null || _d === void 0 ? void 0 : _d.createRequest.message);
450
+ break;
451
+ case "NoReviewersSetForOwnerError":
452
+ cmd.log((_e = resp.data) === null || _e === void 0 ? void 0 : _e.createRequest.message);
453
+ break;
454
+ case "NoReviewersSetForResourceError":
455
+ cmd.log((_f = resp.data) === null || _f === void 0 ? void 0 : _f.createRequest.message);
456
+ break;
457
+ case "NoReviewersSetForGroupError":
458
+ cmd.log((_g = resp.data) === null || _g === void 0 ? void 0 : _g.createRequest.message);
459
+ break;
460
+ case "NoManagerSetForRequestingUserError":
461
+ cmd.log((_h = resp.data) === null || _h === void 0 ? void 0 : _h.createRequest.message);
462
+ break;
463
+ case "MfaInvalidError":
464
+ cmd.log((_j = resp.data) === null || _j === void 0 ? void 0 : _j.createRequest.message);
465
+ break;
466
+ case "BulkRequestTooLargeError":
467
+ cmd.log((_k = resp.data) === null || _k === void 0 ? void 0 : _k.createRequest.message);
468
+ break;
469
+ case "ItemCannotBeRequestedError":
470
+ cmd.log((_l = resp.data) === null || _l === void 0 ? void 0 : _l.createRequest.message);
471
+ break;
472
+ case "UserCannotRequestAccessForTargetGroupError":
473
+ cmd.log((_m = resp.data) === null || _m === void 0 ? void 0 : _m.createRequest.message);
474
+ break;
475
+ case "GroupNestingNotAllowedError":
476
+ cmd.log((_o = resp.data) === null || _o === void 0 ? void 0 : _o.createRequest.message);
477
+ break;
478
+ case "TargetUserHasNestedAccessError":
479
+ cmd.log((_p = resp.data) === null || _p === void 0 ? void 0 : _p.createRequest.message);
480
+ break;
481
+ case "RequestReasonMissingError":
482
+ cmd.log((_q = resp.data) === null || _q === void 0 ? void 0 : _q.createRequest.message);
483
+ break;
484
+ case "RequestFieldValueMissingError":
485
+ cmd.log((_r = resp.data) === null || _r === void 0 ? void 0 : _r.createRequest.message);
486
+ break;
487
+ case "LinkedGroupNotRequestableError":
488
+ cmd.log((_s = resp.data) === null || _s === void 0 ? void 0 : _s.createRequest.message);
489
+ break;
490
+ case "RequestReasonBelowMinLengthError":
491
+ cmd.log((_t = resp.data) === null || _t === void 0 ? void 0 : _t.createRequest.message);
492
+ break;
493
+ }
494
+ }
495
+ catch (error) {
496
+ if (error instanceof Error || typeof error === "string") {
497
+ cmd.error(error);
498
+ }
499
+ }
500
+ }
501
+ const CATALOG_ITEM = (0, graphql_1.graphql)(`
502
+ query GetCatalogItem($uuid: UUID!) {
503
+ catalogItem(id: $uuid) {
504
+ __typename
505
+ ... on Connection {
506
+ id
507
+ displayName
508
+ }
509
+ ... on Resource {
510
+ id
511
+ displayName
512
+ connection {
513
+ id
514
+ displayName
515
+ }
516
+ accessLevels{
517
+ accessLevelName
518
+ accessLevelRemoteId
519
+ }
520
+ }
521
+ ...on Group {
522
+ id
523
+ name
524
+ connection {
525
+ id
526
+ displayName
527
+ }
528
+ accessLevels{
529
+ accessLevelName
530
+ accessLevelRemoteId
531
+ }
532
+ }
533
+ ... on UserFacingError {
534
+ message
535
+ }
536
+ }
537
+ }
538
+ `);
539
+ const ASSOCIATED_ITEMS_QUERY = (0, graphql_1.graphql)(`
540
+ query GetAssociatedItems($resourceId: ResourceId!, $searchQuery: String) {
541
+ resource(input: {
542
+ id: $resourceId
543
+ }) {
544
+ __typename
545
+ ... on ResourceResult {
546
+ __typename
547
+ resource {
548
+ associatedItems(
549
+ first: 200
550
+ filters: {
551
+ searchQuery: {
552
+ contains: $searchQuery
553
+ }
554
+ access: REQUESTABLE
555
+ endUserVisible: true
556
+ entityType: {
557
+ in: [GROUP, RESOURCE]
558
+ }
559
+ }
560
+ ) {
561
+ edges {
562
+ __typename
563
+ ... on ResourceAssociatedItemEdge {
564
+ alias
565
+ node {
566
+ __typename
567
+ id
568
+ name
569
+ ... on Resource {
570
+ accessLevels(
571
+ filters: {
572
+ skipRemoteAccessLevels: false # azure app roles are remote
573
+ }
574
+ ) {
575
+ __typename
576
+ accessLevelName
577
+ accessLevelRemoteId
578
+ }
579
+ }
580
+ }
581
+ }
582
+ }
583
+ }
584
+ }
585
+ }
586
+ ... on ResourceNotFoundError {
587
+ message
588
+ }
589
+ }
590
+ }
591
+ `);
592
+ async function queryAssociatedItems(cmd, client, id, input) {
593
+ var _a, _b;
594
+ try {
595
+ const resp = await client.query({
596
+ query: ASSOCIATED_ITEMS_QUERY,
597
+ variables: {
598
+ resourceId: id || "",
599
+ searchQuery: input || "",
600
+ },
601
+ fetchPolicy: "network-only", // to avoid caching
602
+ });
603
+ switch (resp.data.resource.__typename) {
604
+ case "ResourceResult": {
605
+ const associatedItems = resp.data.resource.resource.associatedItems.edges.filter((edge) => edge.__typename === "ResourceAssociatedItemEdge");
606
+ const initial = [];
607
+ for (const edge of associatedItems) {
608
+ initial.push(...appRolesFromEdge(edge));
609
+ }
610
+ return initial;
611
+ }
612
+ case "ResourceNotFoundError":
613
+ cmd.log((_b = (_a = resp.data) === null || _a === void 0 ? void 0 : _a.resource) === null || _b === void 0 ? void 0 : _b.message);
614
+ break;
615
+ default:
616
+ cmd.error(resp.error || "Unknown error occurred.");
617
+ }
618
+ }
619
+ catch (error) {
620
+ if (error instanceof Error || typeof error === "string") {
621
+ cmd.error(error);
622
+ }
623
+ }
624
+ }
274
625
  // Helper functions
626
+ const selectInstructions = chalk_1.default.dim("↑↓ Navigate Ā· Enter Select Ā· Type to filter");
627
+ const multiSelectInstructions = chalk_1.default.dim("↑↓ Navigate Ā· Space Select Ā· Enter Confirm Ā· Type to filter");
275
628
  async function selectRequestableItems(cmd, client, requestMap) {
276
- const { App } = await inquirer.prompt([
277
- {
278
- name: "App",
279
- message: "Select an app:",
280
- type: "autocomplete",
281
- source: async (answers, input) => {
282
- var _a;
283
- return (_a = (await queryRequestableApps(cmd, client, input))) !== null && _a !== void 0 ? _a : [];
284
- },
285
- pageSize: 15,
629
+ const initial = (await queryRequestableApps(cmd, client, "")) || [];
630
+ const appPrompt = new AutoComplete({
631
+ name: "App",
632
+ message: "Select an app",
633
+ hint: selectInstructions,
634
+ limit: 15,
635
+ choices: initial,
636
+ async suggest(input) {
637
+ const filteredChoices = await queryRequestableApps(cmd, client, input || "");
638
+ return filteredChoices || initial;
286
639
  },
287
- ]);
640
+ });
641
+ const App = await appPrompt.run();
288
642
  // Set the app in the requestMap and call choose assets step
289
643
  if (!(App.id in requestMap)) {
290
644
  requestMap[App.id] = {
645
+ appId: App.id,
291
646
  appName: App.name,
292
647
  assets: {},
293
648
  };
294
649
  }
650
+ if (App.type === "OKTA_APP" || App.type === "AZURE_ENTERPRISE_APP") {
651
+ await chooseOktaAzureRoles(cmd, client, App, requestMap);
652
+ return;
653
+ }
295
654
  await chooseAssets(cmd, client, App.id, requestMap);
296
655
  }
656
+ async function chooseOktaAzureRoles(cmd, client, app, requestMap) {
657
+ const associatedItems = (await queryAssociatedItems(cmd, client, app.id, "")) || [];
658
+ const rolePrompt = new AutoComplete({
659
+ name: "Roles",
660
+ message: `Select a role for ${app.name}:`,
661
+ hint: multiSelectInstructions,
662
+ limit: 15,
663
+ multiple: true,
664
+ async choices(input) {
665
+ if (!input)
666
+ return associatedItems;
667
+ const filteredChoices = await queryAssociatedItems(cmd, client, app.id, input);
668
+ return filteredChoices || associatedItems;
669
+ },
670
+ validate: (answer) => {
671
+ if (answer.length !== 1) {
672
+ return "You must select only one item.";
673
+ }
674
+ return true;
675
+ },
676
+ });
677
+ const Roles = await rolePrompt.run();
678
+ const entry = requestMap[app.id];
679
+ for (const role of Roles) {
680
+ if (!(role.id in entry.assets)) {
681
+ entry.assets[role.id] = {
682
+ assetId: role.id,
683
+ assetName: role.name,
684
+ type: entityTypeFromString(role.type),
685
+ roles: {},
686
+ };
687
+ }
688
+ }
689
+ }
690
+ function appRolesFromEdge(edge) {
691
+ var _a, _b, _c, _d;
692
+ switch (edge.node.__typename) {
693
+ case "Resource": {
694
+ if (edge.node.accessLevels && edge.node.accessLevels.length > 0) {
695
+ return edge.node.accessLevels.map((accessLevel) => ({
696
+ message: accessLevel.accessLevelName || "No Role (Direct access)",
697
+ value: {
698
+ id: edge.node.id + accessLevel.accessLevelRemoteId,
699
+ name: accessLevel.accessLevelName,
700
+ type: graphql_2.EntityType.Resource,
701
+ toString: () => accessLevel.accessLevelName,
702
+ },
703
+ }));
704
+ }
705
+ return [
706
+ {
707
+ message: (_a = edge.alias) !== null && _a !== void 0 ? _a : edge.node.name,
708
+ value: {
709
+ id: edge.node.id,
710
+ name: (_b = edge.alias) !== null && _b !== void 0 ? _b : edge.node.name,
711
+ type: graphql_2.EntityType.Resource,
712
+ toString: () => { var _a; return (_a = edge.alias) !== null && _a !== void 0 ? _a : edge.node.name; },
713
+ },
714
+ },
715
+ ];
716
+ }
717
+ case "Group":
718
+ return [
719
+ {
720
+ message: (_c = edge.alias) !== null && _c !== void 0 ? _c : edge.node.name,
721
+ value: {
722
+ id: edge.node.id,
723
+ name: (_d = edge.alias) !== null && _d !== void 0 ? _d : edge.node.name,
724
+ type: graphql_2.EntityType.Group,
725
+ toString: () => { var _a; return (_a = edge.alias) !== null && _a !== void 0 ? _a : edge.node.name; },
726
+ },
727
+ },
728
+ ];
729
+ }
730
+ }
297
731
  async function chooseAssets(cmd, client, appId, requestMap) {
298
- var _a;
299
- const { Assets } = await inquirer.prompt({
732
+ const initial = (await queryRequestableAssets(cmd, client, appId, "")) || [];
733
+ const assetPrompt = new AutoComplete({
300
734
  name: "Assets",
301
- type: "checkbox",
302
- pageSize: 15,
303
- message: "Select one or more items:",
304
- choices: (_a = (await queryRequestableAssets(cmd, client, appId, undefined))) !== null && _a !== void 0 ? _a : [],
735
+ message: "Select one or more assets:",
736
+ hint: multiSelectInstructions,
737
+ limit: 15,
738
+ multiple: true,
739
+ async choices(input) {
740
+ if (!input) {
741
+ return initial;
742
+ }
743
+ const filteredChoices = await queryRequestableAssets(cmd, client, appId, input);
744
+ return filteredChoices || initial;
745
+ },
305
746
  validate: (answer) => {
306
747
  if (answer.length < 1) {
307
748
  return "You must select at least one item.";
@@ -309,14 +750,17 @@ async function chooseAssets(cmd, client, appId, requestMap) {
309
750
  return true;
310
751
  },
311
752
  });
753
+ const Assets = await assetPrompt.run();
312
754
  const entry = requestMap[appId];
313
755
  for (const asset of Assets) {
314
756
  if (entry === undefined) {
315
- throw new Error(`App ${appId} not found in requestMap`);
757
+ throw new Error(`Error formatting app ${appId} in request`);
316
758
  }
317
759
  if (!(asset.id in entry.assets)) {
318
760
  entry.assets[asset.id] = {
761
+ assetId: asset.id,
319
762
  assetName: asset.name,
763
+ type: asset.type,
320
764
  roles: {},
321
765
  };
322
766
  }
@@ -325,34 +769,38 @@ async function chooseAssets(cmd, client, appId, requestMap) {
325
769
  }
326
770
  async function chooseRoles(cmd, client, appId, assetId, requestMap) {
327
771
  var _a;
328
- const resourceRoles = (_a = (await queryResourceRoles(cmd, client, assetId))) !== null && _a !== void 0 ? _a : [];
329
- if (resourceRoles !== undefined &&
330
- (resourceRoles.length === 0 ||
331
- (resourceRoles.length === 1 && resourceRoles[0].name === ""))) {
772
+ const entry = requestMap[appId];
773
+ const assetEntry = entry === null || entry === void 0 ? void 0 : entry.assets[assetId];
774
+ if (entry === undefined || assetEntry === undefined) {
775
+ throw new Error(`App ${appId} or Asset ${assetId} not found in requestMap`);
776
+ }
777
+ const assetRoles = (_a = (await queryAssetRoles(cmd, client, assetEntry.type, assetId))) !== null && _a !== void 0 ? _a : [];
778
+ if (assetRoles !== undefined &&
779
+ (assetRoles.length === 0 ||
780
+ (assetRoles.length === 1 && assetRoles[0].value.name === ""))) {
332
781
  return;
333
782
  }
334
- const { roles } = await inquirer.prompt({
335
- name: "roles",
336
- type: "checkbox",
337
- message: `Select one or more roles for ${assetId}:`,
338
- choices: resourceRoles,
783
+ const rolePrompt = new AutoComplete({
784
+ name: "Roles",
785
+ message: `Select one or more roles for ${assetEntry.assetName}:`,
786
+ hint: multiSelectInstructions,
787
+ limit: 15,
788
+ multiple: true,
789
+ choices: assetRoles,
339
790
  validate: (answer) => {
340
- if ((resourceRoles === null || resourceRoles === void 0 ? void 0 : resourceRoles.length) > 1 && answer.length < 1) {
341
- return "You must select at least one role.";
791
+ if (answer.length < 1) {
792
+ return "You must select at least one item.";
342
793
  }
343
794
  return true;
344
795
  },
345
796
  });
346
- const entry = requestMap[appId];
347
- const assetEntry = entry === null || entry === void 0 ? void 0 : entry.assets[assetId];
348
- if (entry === undefined || assetEntry === undefined) {
349
- throw new Error(`App ${appId} or Asset ${assetId} not found in requestMap`);
350
- }
797
+ const roles = await rolePrompt.run();
351
798
  if (!assetEntry.roles) {
352
799
  assetEntry.roles = {};
353
800
  }
354
801
  for (const role of roles) {
355
802
  assetEntry.roles[role.id] = {
803
+ roleId: role.id,
356
804
  roleName: role.name,
357
805
  };
358
806
  }
@@ -360,14 +808,12 @@ async function chooseRoles(cmd, client, appId, assetId, requestMap) {
360
808
  async function doneSelectingAssets() {
361
809
  const submitMessage = "āœ… Yes, proceed with request";
362
810
  const addMoreMessage = "āŒ No, add more items";
363
- const { submitOrAdd } = await inquirer.prompt([
364
- {
365
- name: "submitOrAdd",
366
- message: "Is this all you want to request?",
367
- type: "list",
368
- choices: [submitMessage, addMoreMessage],
369
- },
370
- ]);
811
+ const prompt = new Select({
812
+ name: "submitOrAdd",
813
+ message: "Is this all you want to request?",
814
+ choices: [submitMessage, addMoreMessage],
815
+ });
816
+ const submitOrAdd = await prompt.run();
371
817
  return submitOrAdd === submitMessage;
372
818
  }
373
819
  async function setRequestDefaults(cmd, client, metadata) {
@@ -377,18 +823,34 @@ async function setRequestDefaults(cmd, client, metadata) {
377
823
  for (const appNode of Object.values(requestMap)) {
378
824
  for (const [assetId, assetNode] of Object.entries(appNode.assets)) {
379
825
  if (assetNode.roles !== undefined) {
380
- for (const roleId of Object.keys(assetNode.roles)) {
381
- requestedResources.push({
382
- resourceId: assetId,
383
- accessLevelRemoteId: roleId,
384
- });
826
+ const mappedRoles = Object.entries(assetNode.roles).map(([roleId, _roleNode]) => {
827
+ return roleId;
828
+ });
829
+ const roleIds = mappedRoles.length ? mappedRoles : [""];
830
+ for (const roleId of roleIds) {
831
+ switch (assetNode.type) {
832
+ case graphql_2.EntityType.Resource: {
833
+ requestedResources.push({
834
+ resourceId: assetId,
835
+ accessLevelRemoteId: roleId,
836
+ });
837
+ break;
838
+ }
839
+ case graphql_2.EntityType.Group: {
840
+ requestedGroups.push({
841
+ groupId: assetId,
842
+ accessLevelRemoteId: roleId,
843
+ });
844
+ break;
845
+ }
846
+ }
385
847
  }
386
848
  }
387
849
  }
388
850
  }
389
851
  try {
390
852
  const requestDefaults = await queryRequestDefaults(cmd, client, requestedResources, requestedGroups);
391
- if ((requestDefaults === null || requestDefaults === void 0 ? void 0 : requestDefaults.__typename) === "RequestDefaults") {
853
+ if (requestDefaults !== undefined) {
392
854
  metadata.requestDefaults.durationOptions =
393
855
  requestDefaults.durationOptions;
394
856
  metadata.requestDefaults.recommendedDurationInMinutes =
@@ -409,49 +871,133 @@ async function setRequestDefaults(cmd, client, metadata) {
409
871
  }
410
872
  }
411
873
  async function promptForReason(metadata) {
412
- return await inquirer.prompt([
874
+ const { reason } = await prompt([
413
875
  {
414
876
  name: "reason",
415
- message: "I need access to this because...",
877
+ message: "Why do you need access?",
416
878
  type: "input",
417
879
  validate: (answer) => {
418
- if (metadata.requestDefaults.reasonOptional && answer.length < 1) {
880
+ if (!metadata.requestDefaults.reasonOptional && answer.length < 1) {
419
881
  return "A reason for requesting these assets is required.";
420
882
  }
421
883
  return true;
422
884
  },
423
885
  },
424
886
  ]);
887
+ metadata.reason = reason;
425
888
  }
426
889
  async function promptForExpiration(metadata) {
427
890
  var _a, _b;
428
891
  const durations = ((_b = (_a = metadata.requestDefaults) === null || _a === void 0 ? void 0 : _a.durationOptions) === null || _b === void 0 ? void 0 : _b.map((option) => {
892
+ var _a;
893
+ let label = option.label;
894
+ if (option.durationInMinutes ===
895
+ ((_a = metadata.requestDefaults) === null || _a === void 0 ? void 0 : _a.maxDurationInMinutes)) {
896
+ label = `${label} (MAX)`;
897
+ }
898
+ if (option.durationInMinutes ===
899
+ metadata.requestDefaults.recommendedDurationInMinutes) {
900
+ label = `${label} (RECOMMENDED)`;
901
+ }
429
902
  return {
430
- name: option.durationInMinutes ===
431
- metadata.requestDefaults.recommendedDurationInMinutes
432
- ? `${option.label} (Recommended)`
433
- : option.label,
903
+ message: label,
434
904
  value: {
435
- label: option.label,
905
+ label: label,
436
906
  durationInMinutes: option.durationInMinutes,
907
+ toString: () => label,
437
908
  },
438
909
  };
439
910
  })) || [];
440
- // TODO: Sort durations by minutes
441
- // durations = durations.sort(
442
- // durations.filter((option) => option.value.durationInMinutes),
443
- // );
444
- return await inquirer.prompt([
445
- {
446
- name: "expiration",
447
- message: "When should access expire?",
448
- type: "list",
449
- choices: durations,
450
- pageSize: 15,
911
+ // Sort durations by minutes
912
+ durations.sort((a, b) => a.value.durationInMinutes - b.value.durationInMinutes);
913
+ const expirationSelect = new AutoComplete({
914
+ name: "expiration",
915
+ message: "When should access expire?",
916
+ type: "list",
917
+ choices: durations,
918
+ pageSize: 15,
919
+ });
920
+ let selected = await expirationSelect.run();
921
+ switch (selected.label) {
922
+ case "Custom": {
923
+ selected = await setCustomDuration(metadata);
924
+ break;
925
+ }
926
+ case "Permanent": {
927
+ selected.durationInMinutes = undefined;
928
+ break;
929
+ }
930
+ }
931
+ metadata.durationInMinutes = selected.durationInMinutes;
932
+ metadata.durationLabel = selected.label;
933
+ }
934
+ function getDurationNumbers(duration) {
935
+ const d = +duration.days || 0;
936
+ const h = +duration.hours || 0;
937
+ const m = +duration.minutes || 0;
938
+ return { d, h, m };
939
+ }
940
+ function getDurationInMinutes(duration) {
941
+ const { d, h, m } = getDurationNumbers(duration);
942
+ const minutesInDay = 1440; // 24 hours * 60 minutes
943
+ const minutesInHour = 60;
944
+ return d * minutesInDay + h * minutesInHour + m;
945
+ }
946
+ function getDHMFromMinutes(minutes) {
947
+ const label = [];
948
+ const d = Math.floor(minutes / 1440);
949
+ if (d > 0) {
950
+ label.push(`${d}d`);
951
+ }
952
+ const remainingMinutes = minutes % 1440;
953
+ const h = Math.floor(remainingMinutes / 60);
954
+ if (h > 0) {
955
+ label.push(`${h}h`);
956
+ }
957
+ const m = remainingMinutes % 60;
958
+ if (m > 0) {
959
+ label.push(`${m}m`);
960
+ }
961
+ return label.join(" ");
962
+ }
963
+ async function setCustomDuration(metadata) {
964
+ const durationForm = new Form({
965
+ name: "user",
966
+ message: "Please set a custom access duration:",
967
+ choices: [
968
+ { name: "days", message: "Days", initial: "0" },
969
+ { name: "hours", message: "Hours", initial: "0" },
970
+ { name: "minutes", message: "Minutes", initial: "0" },
971
+ ],
972
+ validate: (answer) => {
973
+ var _a, _b, _c;
974
+ const { d, h, m } = getDurationNumbers(answer);
975
+ const durationInMinutes = getDurationInMinutes(answer);
976
+ if (d < 0 || h < 0 || m < 0 || d + h + m === 0 || (h > 23 && m > 59)) {
977
+ return "Please enter a valid duration.";
978
+ }
979
+ if (((_a = metadata.requestDefaults) === null || _a === void 0 ? void 0 : _a.maxDurationInMinutes) &&
980
+ durationInMinutes > ((_b = metadata.requestDefaults) === null || _b === void 0 ? void 0 : _b.maxDurationInMinutes)) {
981
+ const maxDHM = getDHMFromMinutes((_c = metadata.requestDefaults) === null || _c === void 0 ? void 0 : _c.maxDurationInMinutes);
982
+ return `The max duration for the selected assets is ${maxDHM}.`;
983
+ }
984
+ return true;
451
985
  },
452
- ]);
986
+ return: (answer) => {
987
+ return getDurationInMinutes(answer);
988
+ },
989
+ });
990
+ const durationResult = await durationForm.run();
991
+ const { d, h, m } = getDurationNumbers(durationResult);
992
+ const durationInMinutes = getDurationInMinutes(durationResult);
993
+ const durationLabel = getDHMFromMinutes(durationInMinutes);
994
+ return {
995
+ durationInMinutes: durationInMinutes,
996
+ label: durationLabel,
997
+ };
453
998
  }
454
- async function submitFinalRequest(cmd) {
999
+ async function promptRequestSubmission(cmd, metadata) {
1000
+ (0, displays_1.displayFinalRequestSummary)(cmd, metadata);
455
1001
  const submitMessage = "āœ… Yes, submit request";
456
1002
  const cancelMessage = "āŒ No, cancel request";
457
1003
  const { submit } = await inquirer.prompt([
@@ -462,12 +1008,150 @@ async function submitFinalRequest(cmd) {
462
1008
  choices: [submitMessage, cancelMessage],
463
1009
  },
464
1010
  ]);
465
- if (submit === submitMessage) {
466
- const requestLink = "https://dev.opal.dev/requests/sent/05ca5d5f-ea60-4cdb-84e1-7e3c575b2b72"; //TODO: Replace with actual request link
467
- cmd.log("\nšŸŽ‰ Your Access Request has been submitted! Request ID: 1234");
468
- cmd.log(`šŸ” View request status here: ${requestLink}`);
1011
+ switch (submit) {
1012
+ case submitMessage: {
1013
+ return;
1014
+ }
1015
+ case cancelMessage: {
1016
+ cmd.log("🚫 Access Request has been cancelled.");
1017
+ return;
1018
+ }
1019
+ default: {
1020
+ cmd.error("Unknown error occurred.");
1021
+ }
1022
+ }
1023
+ }
1024
+ async function submitFinalRequest(cmd, client, metadata) {
1025
+ var _a, _b, _c, _d;
1026
+ // Build requested assets lists for the mutation
1027
+ const requestedResources = [];
1028
+ const requestedGroups = [];
1029
+ for (const appNode of Object.values(metadata.requestMap)) {
1030
+ // This extraction is different than the one in setRequestDefaults.
1031
+ // Both extract the requestedResources and requestedGroups,
1032
+ // use different formats.
1033
+ for (const [assetId, assetNode] of Object.entries(appNode.assets)) {
1034
+ if (assetNode.roles) {
1035
+ const mappedRoles = Object.entries(assetNode.roles).map(([roleId, _roleNode]) => {
1036
+ return roleId;
1037
+ });
1038
+ const roleIds = mappedRoles.length > 0 ? mappedRoles : [""];
1039
+ for (const roleId of roleIds) {
1040
+ switch (assetNode.type) {
1041
+ case graphql_2.EntityType.Resource: {
1042
+ requestedResources.push({
1043
+ resourceId: assetId,
1044
+ accessLevel: {
1045
+ accessLevelName: ((_b = (_a = assetNode.roles) === null || _a === void 0 ? void 0 : _a[roleId]) === null || _b === void 0 ? void 0 : _b.roleName) || "",
1046
+ accessLevelRemoteId: roleId,
1047
+ },
1048
+ });
1049
+ break;
1050
+ }
1051
+ case graphql_2.EntityType.Group: {
1052
+ requestedGroups.push({
1053
+ groupId: assetId,
1054
+ accessLevel: {
1055
+ accessLevelName: ((_d = (_c = assetNode.roles) === null || _c === void 0 ? void 0 : _c[roleId]) === null || _d === void 0 ? void 0 : _d.roleName) || "",
1056
+ accessLevelRemoteId: roleId,
1057
+ },
1058
+ });
1059
+ break;
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ const resp = await createRequest(cmd, client, requestedResources, requestedGroups, metadata.reason, metadata.durationInMinutes);
1067
+ // Build link to request
1068
+ const configData = (0, config_1.getOrCreateConfigData)(cmd.config.configDir);
1069
+ if (resp === null || resp === void 0 ? void 0 : resp.id) {
1070
+ cmd.log("\nšŸŽ‰ Your Access Request has been submitted!\n");
1071
+ cmd.log(`${chalk_1.default.bold("ID: ")} ${chalk_1.default.cyan(resp === null || resp === void 0 ? void 0 : resp.id)}`);
1072
+ if (resp === null || resp === void 0 ? void 0 : resp.status) {
1073
+ cmd.log((0, displays_1.getStyledStatus)(resp === null || resp === void 0 ? void 0 : resp.status));
1074
+ }
1075
+ const requestLink = `${configData[config_1.urlKey]}/requests/sent/${resp === null || resp === void 0 ? void 0 : resp.id}`;
1076
+ cmd.log(`${chalk_1.default.bold("Link:")} ${chalk_1.default.underline(requestLink)}\n`);
1077
+ }
1078
+ return;
1079
+ }
1080
+ async function bypassRequestSelection(cmd, client, flagValue, metadata) {
1081
+ var _a, _b;
1082
+ try {
1083
+ // Query Catalog Item endpoint to identify what the id belongs to (resource or group)
1084
+ for (const id of flagValue) {
1085
+ const [assetId, roleName] = id.split(":");
1086
+ const resp = await client.query({
1087
+ query: CATALOG_ITEM,
1088
+ variables: {
1089
+ uuid: assetId || "",
1090
+ },
1091
+ fetchPolicy: "network-only", // to avoid caching
1092
+ });
1093
+ switch (resp.data.catalogItem.__typename) {
1094
+ case "Group":
1095
+ case "Resource": {
1096
+ const item = resp.data.catalogItem;
1097
+ const assetName = item.__typename === "Resource" ? item.displayName : item.name;
1098
+ const requestableRoles = (item.accessLevels || [])
1099
+ // TODO: Support okta azure apps ?.filter((role) => role.accessLevelName !== "") // This assumes length == 1
1100
+ .map((role) => ({
1101
+ id: role.accessLevelRemoteId,
1102
+ name: role.accessLevelName,
1103
+ }));
1104
+ const appId = ((_a = item.connection) === null || _a === void 0 ? void 0 : _a.id) || "";
1105
+ if (!(appId in metadata.requestMap)) {
1106
+ metadata.requestMap[appId] = {
1107
+ appName: ((_b = item.connection) === null || _b === void 0 ? void 0 : _b.displayName) || "",
1108
+ appId: appId,
1109
+ assets: {},
1110
+ };
1111
+ }
1112
+ const assetEntry = metadata.requestMap[appId].assets[id];
1113
+ if (!assetEntry) {
1114
+ metadata.requestMap[appId].assets[assetId] = {
1115
+ assetId: assetId,
1116
+ assetName: assetName,
1117
+ type: entityTypeFromString(item.__typename),
1118
+ roles: {},
1119
+ };
1120
+ }
1121
+ if (requestableRoles.length > 0 &&
1122
+ !(requestableRoles.length === 1 && requestableRoles[0].name === "")) {
1123
+ const selectedRole = requestableRoles.find((role) => role.name === roleName);
1124
+ if (selectedRole !== undefined) {
1125
+ if (!metadata.requestMap[appId].assets[assetId].roles) {
1126
+ metadata.requestMap[appId].assets[assetId].roles = {};
1127
+ }
1128
+ metadata.requestMap[appId].assets[assetId].roles[selectedRole.id] = {
1129
+ roleId: selectedRole.id,
1130
+ roleName: selectedRole.name,
1131
+ };
1132
+ }
1133
+ else {
1134
+ cmd.error(`Access level specified does not match one of ${assetName}'s defined access levels:${requestableRoles.map((role) => role.name)}`);
1135
+ }
1136
+ }
1137
+ break;
1138
+ }
1139
+ default:
1140
+ cmd.error("Invalid asset id was passed in using the --id flag.");
1141
+ }
1142
+ }
1143
+ }
1144
+ catch (error) {
1145
+ if (error instanceof Error || typeof error === "string") {
1146
+ cmd.error(error);
1147
+ }
469
1148
  }
470
- else {
471
- cmd.log("🚫 Access Request has been cancelled.");
1149
+ return;
1150
+ }
1151
+ function bypassDuration(cmd, duration, metadata) {
1152
+ const maxDuration = metadata.requestDefaults.maxDurationInMinutes;
1153
+ if (maxDuration && duration > maxDuration) {
1154
+ cmd.error(`The requested duration exceeds the allowed limit of ${maxDuration}`);
472
1155
  }
1156
+ metadata.durationInMinutes = duration;
473
1157
  }