opal-security 3.1.1-beta.4ab1987 → 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,19 +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
- const { AutoComplete } = require("enquirer");
16
- 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() {
17
31
  // Initialize with empty defaults
18
32
  const requestDefaults = {
19
33
  durationOptions: [],
@@ -29,6 +43,9 @@ function createEmptyRequestMetadata() {
29
43
  return {
30
44
  requestMap,
31
45
  requestDefaults,
46
+ durationLabel: "",
47
+ durationInMinutes: 0,
48
+ reason: "",
32
49
  };
33
50
  }
34
51
  // Queries and Mutations
@@ -74,19 +91,23 @@ async function queryRequestableApps(cmd, client, input) {
74
91
  });
75
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) => {
76
93
  let type = undefined;
77
- if (edge.node.__typename === "Resource") {
78
- type = edge.node.resourceType;
79
- }
80
- if (edge.node.__typename === "Connection") {
81
- type = edge.node.connectionType;
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;
82
103
  }
83
- const label = `${edge.node.displayName} (${type})`;
84
104
  return {
85
- message: label,
105
+ message: `${edge.node.displayName} [${type}]`,
86
106
  value: {
87
107
  id: edge.node.id,
88
- name: label,
89
- toString: () => label,
108
+ name: edge.node.displayName,
109
+ type: type,
110
+ toString: () => edge.node.displayName,
90
111
  },
91
112
  };
92
113
  });
@@ -153,13 +174,12 @@ async function queryRequestableAssets(cmd, client, appId, input) {
153
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);
154
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);
155
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);
156
- const label = `${name} (${type})`;
157
177
  return {
158
- message: label,
178
+ message: `${name} [${type}]`,
159
179
  value: {
160
- name: label,
161
- id: id,
162
- type: ((_g = item.resource) === null || _g === void 0 ? void 0 : _g.__typename) || ((_h = item.group) === null || _h === void 0 ? void 0 : _h.__typename),
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)),
163
183
  },
164
184
  };
165
185
  });
@@ -232,10 +252,10 @@ async function queryAssetRoles(cmd, client, assetType, assetId) {
232
252
  case "ResourceAccessLevelsResult":
233
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) => {
234
254
  return {
235
- name: role.accessLevelName,
255
+ message: role.accessLevelName || "",
236
256
  value: {
237
- name: role.accessLevelName,
238
- id: role.accessLevelRemoteId,
257
+ name: role.accessLevelName || "",
258
+ id: role.accessLevelRemoteId || "",
239
259
  },
240
260
  };
241
261
  });
@@ -261,7 +281,7 @@ async function queryAssetRoles(cmd, client, assetType, assetId) {
261
281
  case "GroupAccessLevelsResult":
262
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) => {
263
283
  return {
264
- name: role.accessLevelName,
284
+ message: role.accessLevelName,
265
285
  value: {
266
286
  name: role.accessLevelName,
267
287
  id: role.accessLevelRemoteId,
@@ -291,16 +311,18 @@ const REQUEST_DEFAULTS_QUERY = (0, graphql_1.graphql)(`
291
311
  requestedGroups: $requestedGroups,
292
312
  }
293
313
  ) {
294
- durationOptions {
295
- durationInMinutes
296
- label
314
+ ... on RequestDefaults {
315
+ durationOptions {
316
+ durationInMinutes
317
+ label
318
+ }
319
+ recommendedDurationInMinutes
320
+ defaultDurationInMinutes
321
+ maxDurationInMinutes
322
+ requireSupportTicket
323
+ reasonOptional
324
+ requesterIsAdmin
297
325
  }
298
- recommendedDurationInMinutes
299
- defaultDurationInMinutes
300
- maxDurationInMinutes
301
- requireSupportTicket
302
- reasonOptional
303
- requesterIsAdmin
304
326
  }
305
327
  }`);
306
328
  async function queryRequestDefaults(cmd, client, requestedResources, requestedGroups) {
@@ -321,39 +343,405 @@ async function queryRequestDefaults(cmd, client, requestedResources, requestedGr
321
343
  }
322
344
  }
323
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
+ }
324
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");
325
628
  async function selectRequestableItems(cmd, client, requestMap) {
326
- const initialChoices = (await queryRequestableApps(cmd, client, "")) || [];
629
+ const initial = (await queryRequestableApps(cmd, client, "")) || [];
327
630
  const appPrompt = new AutoComplete({
328
631
  name: "App",
329
- message: "Select an app:",
632
+ message: "Select an app",
633
+ hint: selectInstructions,
330
634
  limit: 15,
331
- choices: initialChoices,
635
+ choices: initial,
332
636
  async suggest(input) {
333
637
  const filteredChoices = await queryRequestableApps(cmd, client, input || "");
334
- return filteredChoices || initialChoices;
638
+ return filteredChoices || initial;
335
639
  },
336
640
  });
337
641
  const App = await appPrompt.run();
338
642
  // Set the app in the requestMap and call choose assets step
339
643
  if (!(App.id in requestMap)) {
340
644
  requestMap[App.id] = {
645
+ appId: App.id,
341
646
  appName: App.name,
342
647
  assets: {},
343
648
  };
344
649
  }
650
+ if (App.type === "OKTA_APP" || App.type === "AZURE_ENTERPRISE_APP") {
651
+ await chooseOktaAzureRoles(cmd, client, App, requestMap);
652
+ return;
653
+ }
345
654
  await chooseAssets(cmd, client, App.id, requestMap);
346
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
+ }
347
731
  async function chooseAssets(cmd, client, appId, requestMap) {
348
- const initialChoices = queryRequestableAssets(cmd, client, appId, "") || [];
732
+ const initial = (await queryRequestableAssets(cmd, client, appId, "")) || [];
349
733
  const assetPrompt = new AutoComplete({
350
734
  name: "Assets",
351
735
  message: "Select one or more assets:",
736
+ hint: multiSelectInstructions,
352
737
  limit: 15,
353
738
  multiple: true,
354
739
  async choices(input) {
740
+ if (!input) {
741
+ return initial;
742
+ }
355
743
  const filteredChoices = await queryRequestableAssets(cmd, client, appId, input);
356
- return filteredChoices || initialChoices;
744
+ return filteredChoices || initial;
357
745
  },
358
746
  validate: (answer) => {
359
747
  if (answer.length < 1) {
@@ -370,6 +758,7 @@ async function chooseAssets(cmd, client, appId, requestMap) {
370
758
  }
371
759
  if (!(asset.id in entry.assets)) {
372
760
  entry.assets[asset.id] = {
761
+ assetId: asset.id,
373
762
  assetName: asset.name,
374
763
  type: asset.type,
375
764
  roles: {},
@@ -382,32 +771,36 @@ async function chooseRoles(cmd, client, appId, assetId, requestMap) {
382
771
  var _a;
383
772
  const entry = requestMap[appId];
384
773
  const assetEntry = entry === null || entry === void 0 ? void 0 : entry.assets[assetId];
385
- const resourceRoles = (_a = (await queryAssetRoles(cmd, client, assetEntry.type, assetId))) !== null && _a !== void 0 ? _a : [];
386
- if (resourceRoles !== undefined &&
387
- (resourceRoles.length === 0 ||
388
- (resourceRoles.length === 1 && resourceRoles[0].name === ""))) {
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 === ""))) {
389
781
  return;
390
782
  }
391
- const { roles } = await inquirer.prompt({
392
- name: "roles",
393
- type: "checkbox",
394
- message: `Select one or more roles for ${assetId}:`,
395
- 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,
396
790
  validate: (answer) => {
397
- if ((resourceRoles === null || resourceRoles === void 0 ? void 0 : resourceRoles.length) > 1 && answer.length < 1) {
398
- return "You must select at least one role.";
791
+ if (answer.length < 1) {
792
+ return "You must select at least one item.";
399
793
  }
400
794
  return true;
401
795
  },
402
796
  });
403
- if (entry === undefined || assetEntry === undefined) {
404
- throw new Error(`App ${appId} or Asset ${assetId} not found in requestMap`);
405
- }
797
+ const roles = await rolePrompt.run();
406
798
  if (!assetEntry.roles) {
407
799
  assetEntry.roles = {};
408
800
  }
409
801
  for (const role of roles) {
410
802
  assetEntry.roles[role.id] = {
803
+ roleId: role.id,
411
804
  roleName: role.name,
412
805
  };
413
806
  }
@@ -415,14 +808,12 @@ async function chooseRoles(cmd, client, appId, assetId, requestMap) {
415
808
  async function doneSelectingAssets() {
416
809
  const submitMessage = "āœ… Yes, proceed with request";
417
810
  const addMoreMessage = "āŒ No, add more items";
418
- const { submitOrAdd } = await inquirer.prompt([
419
- {
420
- name: "submitOrAdd",
421
- message: "Is this all you want to request?",
422
- type: "list",
423
- choices: [submitMessage, addMoreMessage],
424
- },
425
- ]);
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();
426
817
  return submitOrAdd === submitMessage;
427
818
  }
428
819
  async function setRequestDefaults(cmd, client, metadata) {
@@ -432,18 +823,34 @@ async function setRequestDefaults(cmd, client, metadata) {
432
823
  for (const appNode of Object.values(requestMap)) {
433
824
  for (const [assetId, assetNode] of Object.entries(appNode.assets)) {
434
825
  if (assetNode.roles !== undefined) {
435
- for (const roleId of Object.keys(assetNode.roles)) {
436
- requestedResources.push({
437
- resourceId: assetId,
438
- accessLevelRemoteId: roleId,
439
- });
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
+ }
440
847
  }
441
848
  }
442
849
  }
443
850
  }
444
851
  try {
445
852
  const requestDefaults = await queryRequestDefaults(cmd, client, requestedResources, requestedGroups);
446
- if ((requestDefaults === null || requestDefaults === void 0 ? void 0 : requestDefaults.__typename) === "RequestDefaults") {
853
+ if (requestDefaults !== undefined) {
447
854
  metadata.requestDefaults.durationOptions =
448
855
  requestDefaults.durationOptions;
449
856
  metadata.requestDefaults.recommendedDurationInMinutes =
@@ -464,49 +871,133 @@ async function setRequestDefaults(cmd, client, metadata) {
464
871
  }
465
872
  }
466
873
  async function promptForReason(metadata) {
467
- return await inquirer.prompt([
874
+ const { reason } = await prompt([
468
875
  {
469
876
  name: "reason",
470
- message: "I need access to this because...",
877
+ message: "Why do you need access?",
471
878
  type: "input",
472
879
  validate: (answer) => {
473
- if (metadata.requestDefaults.reasonOptional && answer.length < 1) {
880
+ if (!metadata.requestDefaults.reasonOptional && answer.length < 1) {
474
881
  return "A reason for requesting these assets is required.";
475
882
  }
476
883
  return true;
477
884
  },
478
885
  },
479
886
  ]);
887
+ metadata.reason = reason;
480
888
  }
481
889
  async function promptForExpiration(metadata) {
482
890
  var _a, _b;
483
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
+ }
484
902
  return {
485
- name: option.durationInMinutes ===
486
- metadata.requestDefaults.recommendedDurationInMinutes
487
- ? `${option.label} (Recommended)`
488
- : option.label,
903
+ message: label,
489
904
  value: {
490
- label: option.label,
905
+ label: label,
491
906
  durationInMinutes: option.durationInMinutes,
907
+ toString: () => label,
492
908
  },
493
909
  };
494
910
  })) || [];
495
- // TODO: Sort durations by minutes
496
- // durations = durations.sort(
497
- // durations.filter((option) => option.value.durationInMinutes),
498
- // );
499
- return await inquirer.prompt([
500
- {
501
- name: "expiration",
502
- message: "When should access expire?",
503
- type: "list",
504
- choices: durations,
505
- 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;
506
985
  },
507
- ]);
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
+ };
508
998
  }
509
- async function submitFinalRequest(cmd) {
999
+ async function promptRequestSubmission(cmd, metadata) {
1000
+ (0, displays_1.displayFinalRequestSummary)(cmd, metadata);
510
1001
  const submitMessage = "āœ… Yes, submit request";
511
1002
  const cancelMessage = "āŒ No, cancel request";
512
1003
  const { submit } = await inquirer.prompt([
@@ -517,12 +1008,150 @@ async function submitFinalRequest(cmd) {
517
1008
  choices: [submitMessage, cancelMessage],
518
1009
  },
519
1010
  ]);
520
- if (submit === submitMessage) {
521
- const requestLink = "https://dev.opal.dev/requests/sent/05ca5d5f-ea60-4cdb-84e1-7e3c575b2b72"; //TODO: Replace with actual request link
522
- cmd.log("\nšŸŽ‰ Your Access Request has been submitted! Request ID: 1234");
523
- 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
+ }
524
1022
  }
525
- else {
526
- cmd.log("🚫 Access Request has been cancelled.");
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
+ }
1148
+ }
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}`);
527
1155
  }
1156
+ metadata.durationInMinutes = duration;
528
1157
  }