opal-security 3.1.1-beta.778ef29 → 3.1.1-beta.7e1cc21

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