opal-security 3.0.0 → 3.0.1-beta.4f1a7ce
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -44
- package/bin/run +1 -1
- package/lib/commands/aws/identity.d.ts +1 -1
- package/lib/commands/aws/identity.js +2 -2
- package/lib/commands/clear-auth-provider.d.ts +1 -1
- package/lib/commands/clear-auth-provider.js +3 -3
- package/lib/commands/curl-example.d.ts +1 -1
- package/lib/commands/curl-example.js +2 -2
- package/lib/commands/iam-roles/start.d.ts +1 -1
- package/lib/commands/iam-roles/start.js +14 -14
- package/lib/commands/kube-roles/start.d.ts +1 -1
- package/lib/commands/kube-roles/start.js +10 -10
- package/lib/commands/login.d.ts +1 -1
- package/lib/commands/login.js +71 -63
- package/lib/commands/logout.d.ts +1 -1
- package/lib/commands/logout.js +3 -3
- package/lib/commands/postgres-instances/start.d.ts +1 -1
- package/lib/commands/postgres-instances/start.js +35 -34
- package/lib/commands/request/create.d.ts +6 -0
- package/lib/commands/request/create.js +37 -0
- package/lib/commands/request/get.d.ts +6 -0
- package/lib/commands/request/get.js +13 -0
- package/lib/commands/request/list.d.ts +7 -0
- package/lib/commands/request/list.js +14 -0
- package/lib/commands/resources/get.d.ts +1 -1
- package/lib/commands/resources/get.js +11 -4
- package/lib/commands/set-auth-provider.d.ts +1 -1
- package/lib/commands/set-auth-provider.js +6 -4
- package/lib/commands/set-custom-header.d.ts +1 -1
- package/lib/commands/set-custom-header.js +5 -3
- package/lib/commands/set-token.d.ts +1 -1
- package/lib/commands/set-token.js +26 -19
- package/lib/commands/set-url.d.ts +1 -1
- package/lib/commands/set-url.js +13 -12
- package/lib/commands/ssh/copyFrom.d.ts +1 -1
- package/lib/commands/ssh/copyFrom.js +13 -13
- package/lib/commands/ssh/copyTo.d.ts +1 -1
- package/lib/commands/ssh/copyTo.js +13 -13
- package/lib/commands/ssh/start.d.ts +1 -1
- package/lib/commands/ssh/start.js +14 -15
- package/lib/graphql/fragment-masking.d.ts +19 -0
- package/lib/graphql/fragment-masking.js +21 -0
- package/lib/graphql/gql.d.ts +46 -0
- package/lib/graphql/gql.js +14 -0
- package/lib/graphql/graphql.d.ts +11476 -0
- package/lib/graphql/graphql.js +1819 -0
- package/lib/graphql/index.d.ts +2 -0
- package/lib/graphql/index.js +5 -0
- package/lib/handler.d.ts +5 -5
- package/lib/handler.js +7 -7
- package/lib/index.d.ts +1 -1
- package/lib/lib/apollo.d.ts +3 -2
- package/lib/lib/apollo.js +59 -46
- package/lib/lib/aws.js +15 -12
- package/lib/lib/cmd.d.ts +4 -6
- package/lib/lib/cmd.js +11 -11
- package/lib/lib/config.js +14 -14
- package/lib/lib/credentials/index.d.ts +1 -1
- package/lib/lib/credentials/index.js +6 -6
- package/lib/lib/credentials/keychain.js +5 -5
- package/lib/lib/credentials/localEncryption.d.ts +2 -2
- package/lib/lib/credentials/localEncryption.js +33 -24
- package/lib/lib/flags.js +9 -9
- package/lib/lib/requests.d.ts +22 -0
- package/lib/lib/requests.js +274 -0
- package/lib/lib/resources.d.ts +2 -2
- package/lib/lib/resources.js +29 -23
- package/lib/lib/sessions.d.ts +2 -2
- package/lib/lib/sessions.js +18 -17
- package/lib/lib/ssh.d.ts +1 -1
- package/lib/lib/ssh.js +8 -8
- package/lib/lib/util.d.ts +0 -1
- package/lib/lib/util.js +13 -13
- package/lib/types.d.ts +1787 -1787
- package/lib/utils/displays.d.ts +5 -0
- package/lib/utils/displays.js +65 -0
- package/lib/utils/utils.d.ts +1 -0
- package/lib/utils/utils.js +18 -0
- package/oclif.manifest.json +70 -3
- package/package.json +25 -29
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getSecretFromConfig = exports.setSecretInConfig = void 0;
|
|
4
|
-
const
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const argon2 = require("argon2");
|
|
6
|
-
const
|
|
6
|
+
const inquirer = require("inquirer");
|
|
7
7
|
const config_1 = require("../config");
|
|
8
|
-
const ENCRYPTION_ALGORITHM =
|
|
8
|
+
const ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
9
9
|
/**
|
|
10
10
|
* On linux, windows, and WSL, we can't easily work with the system keychain
|
|
11
11
|
* So instead, we store the token encrypted in an `accessTokenDetails` block in the CLI's config file
|
|
@@ -20,9 +20,9 @@ let password;
|
|
|
20
20
|
const promptForPassword = async (msg) => {
|
|
21
21
|
const { customKey } = await inquirer.prompt([
|
|
22
22
|
{
|
|
23
|
-
name:
|
|
23
|
+
name: "customKey",
|
|
24
24
|
message: msg,
|
|
25
|
-
type:
|
|
25
|
+
type: "password",
|
|
26
26
|
validate: (key) => key.length > 0,
|
|
27
27
|
},
|
|
28
28
|
]);
|
|
@@ -30,44 +30,53 @@ const promptForPassword = async (msg) => {
|
|
|
30
30
|
};
|
|
31
31
|
const setSecretInConfig = async (command, configData, secret) => {
|
|
32
32
|
if (!password) {
|
|
33
|
-
password = await promptForPassword(
|
|
33
|
+
password = await promptForPassword("Enter a password to encrypt your credentials: ");
|
|
34
34
|
}
|
|
35
|
-
const salt = (0,
|
|
36
|
-
const key = await argon2.hash(password, {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const salt = (0, node_crypto_1.randomBytes)(24);
|
|
36
|
+
const key = await argon2.hash(password, {
|
|
37
|
+
hashLength: 32,
|
|
38
|
+
raw: true,
|
|
39
|
+
salt: salt,
|
|
40
|
+
});
|
|
41
|
+
const iv = (0, node_crypto_1.randomBytes)(12);
|
|
42
|
+
const cipher = (0, node_crypto_1.createCipheriv)(ENCRYPTION_ALGORITHM, key, iv);
|
|
43
|
+
let secretEncrypted = cipher.update(secret, "utf8", "base64");
|
|
44
|
+
secretEncrypted += cipher.final("base64");
|
|
41
45
|
// In addition to storing the encrypted text, we need to store the argon2 salt, and AES authTag + IV so we can decrypt
|
|
42
46
|
configData.creds.accessTokenDetails = {
|
|
43
47
|
encryptedText: secretEncrypted,
|
|
44
|
-
authTag: cipher.getAuthTag().toString(
|
|
45
|
-
salt: salt.toString(
|
|
46
|
-
iv: iv.toString(
|
|
48
|
+
authTag: cipher.getAuthTag().toString("base64"),
|
|
49
|
+
salt: salt.toString("base64"),
|
|
50
|
+
iv: iv.toString("base64"),
|
|
47
51
|
};
|
|
48
52
|
(0, config_1.writeConfigData)(command.config.configDir, configData);
|
|
49
53
|
};
|
|
50
54
|
exports.setSecretInConfig = setSecretInConfig;
|
|
51
55
|
const getSecretFromConfig = async (credsConfig) => {
|
|
52
|
-
let secret;
|
|
56
|
+
let secret = "";
|
|
53
57
|
if (credsConfig === null || credsConfig === void 0 ? void 0 : credsConfig.accessTokenDetails) {
|
|
54
58
|
if (!password) {
|
|
55
|
-
password = await promptForPassword(
|
|
59
|
+
password = await promptForPassword("Enter the password used to encrypt your credentials: ");
|
|
56
60
|
}
|
|
57
|
-
const salt = Buffer.from(credsConfig.accessTokenDetails.salt,
|
|
58
|
-
const iv = Buffer.from(credsConfig.accessTokenDetails.iv,
|
|
59
|
-
const key = await argon2.hash(password, {
|
|
61
|
+
const salt = Buffer.from(credsConfig.accessTokenDetails.salt, "base64");
|
|
62
|
+
const iv = Buffer.from(credsConfig.accessTokenDetails.iv, "base64");
|
|
63
|
+
const key = await argon2.hash(password, {
|
|
64
|
+
hashLength: 32,
|
|
65
|
+
raw: true,
|
|
66
|
+
salt: salt,
|
|
67
|
+
});
|
|
60
68
|
try {
|
|
61
|
-
const decipher = (0,
|
|
62
|
-
decipher.setAuthTag(Buffer.from(credsConfig.accessTokenDetails.authTag,
|
|
63
|
-
secret = decipher.update(credsConfig.accessTokenDetails.encryptedText,
|
|
64
|
-
secret += decipher.final(
|
|
69
|
+
const decipher = (0, node_crypto_1.createDecipheriv)(ENCRYPTION_ALGORITHM, key, iv);
|
|
70
|
+
decipher.setAuthTag(Buffer.from(credsConfig.accessTokenDetails.authTag, "base64"));
|
|
71
|
+
secret = decipher.update(credsConfig.accessTokenDetails.encryptedText, "base64", "utf8");
|
|
72
|
+
secret += decipher.final("utf8");
|
|
65
73
|
}
|
|
66
74
|
catch (error) {
|
|
67
75
|
console.error("ERROR: Failed to decrypt local credentials.\n" +
|
|
68
76
|
" Check that you entered the correct password, or re-authenticate with `opal login`");
|
|
69
77
|
process.exit(1);
|
|
70
78
|
}
|
|
79
|
+
// biome-ignore lint/performance/noDelete: add tests for this function and remove this eventually
|
|
71
80
|
delete credsConfig.accessTokenDetails;
|
|
72
81
|
}
|
|
73
82
|
return secret;
|
package/lib/lib/flags.js
CHANGED
|
@@ -3,24 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SHARED_FLAGS = void 0;
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
exports.SHARED_FLAGS = {
|
|
6
|
-
help: core_1.Flags.help({ char:
|
|
6
|
+
help: core_1.Flags.help({ char: "h" }),
|
|
7
7
|
id: core_1.Flags.string({
|
|
8
8
|
multiple: false,
|
|
9
|
-
char:
|
|
10
|
-
description:
|
|
9
|
+
char: "i",
|
|
10
|
+
description: "The Opal ID of the resource. You can find this from the URL, e.g. https://opal.dev/resources/[ID]",
|
|
11
11
|
}),
|
|
12
12
|
accessLevelRemoteId: core_1.Flags.string({
|
|
13
13
|
multiple: false,
|
|
14
|
-
char:
|
|
15
|
-
description:
|
|
14
|
+
char: "a",
|
|
15
|
+
description: "The remote ID of the access level with which to access the resource.",
|
|
16
16
|
}),
|
|
17
17
|
sessionId: core_1.Flags.string({
|
|
18
18
|
multiple: false,
|
|
19
|
-
char:
|
|
20
|
-
description:
|
|
19
|
+
char: "s",
|
|
20
|
+
description: "The Opal ID of the session to connect to. Uses an existing session that was created via the web flow.",
|
|
21
21
|
}),
|
|
22
22
|
refresh: core_1.Flags.boolean({
|
|
23
|
-
char:
|
|
24
|
-
description:
|
|
23
|
+
char: "r",
|
|
24
|
+
description: "Starts a new session even if one already exists. Useful if a session is about to expire.",
|
|
25
25
|
}),
|
|
26
26
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NormalizedCacheObject } from "@apollo/client/core";
|
|
2
|
+
import type { ApolloClient } from "@apollo/client/core/ApolloClient";
|
|
3
|
+
import type { Command } from "@oclif/core/lib/command";
|
|
4
|
+
export interface AppNode {
|
|
5
|
+
appName: string;
|
|
6
|
+
assets: Map<string, AssetNode>;
|
|
7
|
+
}
|
|
8
|
+
export interface AssetNode {
|
|
9
|
+
assetName: string;
|
|
10
|
+
roles?: Map<string, RoleNode>;
|
|
11
|
+
}
|
|
12
|
+
export interface RoleNode {
|
|
13
|
+
roleName: string;
|
|
14
|
+
}
|
|
15
|
+
export type RequestMap = Map<string, AppNode>;
|
|
16
|
+
export declare function selectRequestableItems(cmd: Command, client: ApolloClient<NormalizedCacheObject>, requestMap: RequestMap): Promise<void>;
|
|
17
|
+
export declare function chooseAssets(cmd: Command, client: ApolloClient<NormalizedCacheObject>, appId: string, requestMap: RequestMap): Promise<void>;
|
|
18
|
+
export declare function chooseRoles(appId: string, assetId: string, requestMap: RequestMap): Promise<void>;
|
|
19
|
+
export declare function doneSelectingAssets(): Promise<boolean>;
|
|
20
|
+
export declare function promptForReason(): Promise<any>;
|
|
21
|
+
export declare function promptForExpiration(): Promise<any>;
|
|
22
|
+
export declare function submitFinalRequest(cmd: Command): Promise<void>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectRequestableItems = selectRequestableItems;
|
|
4
|
+
exports.chooseAssets = chooseAssets;
|
|
5
|
+
exports.chooseRoles = chooseRoles;
|
|
6
|
+
exports.doneSelectingAssets = doneSelectingAssets;
|
|
7
|
+
exports.promptForReason = promptForReason;
|
|
8
|
+
exports.promptForExpiration = promptForExpiration;
|
|
9
|
+
exports.submitFinalRequest = submitFinalRequest;
|
|
10
|
+
const inquirer = require("inquirer");
|
|
11
|
+
const graphql_1 = require("../graphql");
|
|
12
|
+
inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt"));
|
|
13
|
+
// Queries and Mutations
|
|
14
|
+
const GET_REQUESTABLE_APPS_QUERY = (0, graphql_1.graphql)(`
|
|
15
|
+
query GetRequestableAppsQuery($searchQuery: String) {
|
|
16
|
+
appsV2(
|
|
17
|
+
filters: {
|
|
18
|
+
access: REQUESTABLE
|
|
19
|
+
searchQuery: $searchQuery
|
|
20
|
+
}
|
|
21
|
+
) @connection(key: "paginated-app-dropdown") {
|
|
22
|
+
edges {
|
|
23
|
+
node {
|
|
24
|
+
id
|
|
25
|
+
displayName
|
|
26
|
+
... on Connection {
|
|
27
|
+
connectionType
|
|
28
|
+
}
|
|
29
|
+
... on Resource {
|
|
30
|
+
resourceType
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
pageInfo {
|
|
35
|
+
hasNextPage
|
|
36
|
+
hasPreviousPage
|
|
37
|
+
startCursor
|
|
38
|
+
endCursor
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`);
|
|
43
|
+
const GET_ASSETS_QUERY = (0, graphql_1.graphql)(`
|
|
44
|
+
query PaginatedEntityDropdown(
|
|
45
|
+
$id: UUID!
|
|
46
|
+
$searchQuery: String
|
|
47
|
+
) {
|
|
48
|
+
app(id: $id) {
|
|
49
|
+
__typename
|
|
50
|
+
... on App {
|
|
51
|
+
id
|
|
52
|
+
items(
|
|
53
|
+
input: {
|
|
54
|
+
access: REQUESTABLE
|
|
55
|
+
searchQuery: $searchQuery
|
|
56
|
+
includeOnlyRequestable: true
|
|
57
|
+
}
|
|
58
|
+
) {
|
|
59
|
+
items {
|
|
60
|
+
key
|
|
61
|
+
resource {
|
|
62
|
+
id
|
|
63
|
+
name
|
|
64
|
+
}
|
|
65
|
+
group {
|
|
66
|
+
id
|
|
67
|
+
name
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
cursor
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`);
|
|
76
|
+
async function queryRequestableApps(cmd, client, input) {
|
|
77
|
+
var _a, _b;
|
|
78
|
+
try {
|
|
79
|
+
const resp = await client.query({
|
|
80
|
+
query: GET_REQUESTABLE_APPS_QUERY,
|
|
81
|
+
variables: {
|
|
82
|
+
searchQuery: input || "",
|
|
83
|
+
},
|
|
84
|
+
fetchPolicy: "network-only", // to avoid caching
|
|
85
|
+
});
|
|
86
|
+
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) => {
|
|
87
|
+
let type = undefined;
|
|
88
|
+
if (edge.node.__typename === "Resource") {
|
|
89
|
+
type = edge.node.resourceType;
|
|
90
|
+
}
|
|
91
|
+
if (edge.node.__typename === "Connection") {
|
|
92
|
+
type = edge.node.connectionType;
|
|
93
|
+
}
|
|
94
|
+
const label = `${edge.node.displayName} (${type})`;
|
|
95
|
+
return {
|
|
96
|
+
name: label,
|
|
97
|
+
value: {
|
|
98
|
+
id: edge.node.id,
|
|
99
|
+
name: label,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof Error || typeof error === "string") {
|
|
106
|
+
cmd.error(error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function queryRequestableAssets(cmd, client, appId, input) {
|
|
111
|
+
var _a, _b, _c, _d;
|
|
112
|
+
try {
|
|
113
|
+
const resp = await client.query({
|
|
114
|
+
query: GET_ASSETS_QUERY,
|
|
115
|
+
variables: {
|
|
116
|
+
id: appId || "",
|
|
117
|
+
searchQuery: input || "",
|
|
118
|
+
},
|
|
119
|
+
fetchPolicy: "network-only", // to avoid caching
|
|
120
|
+
});
|
|
121
|
+
// no fall through doesn't consider process.exit();
|
|
122
|
+
let x;
|
|
123
|
+
switch (resp.data.app.__typename) {
|
|
124
|
+
case "App":
|
|
125
|
+
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) => {
|
|
126
|
+
var _a, _b, _c, _d, _e, _f;
|
|
127
|
+
const name = ((_a = item.resource) === null || _a === void 0 ? void 0 : _a.name) || ((_b = item.group) === null || _b === void 0 ? void 0 : _b.name);
|
|
128
|
+
const id = ((_c = item.resource) === null || _c === void 0 ? void 0 : _c.id) || ((_d = item.group) === null || _d === void 0 ? void 0 : _d.id);
|
|
129
|
+
const type = ((_e = item.resource) === null || _e === void 0 ? void 0 : _e.__typename) || ((_f = item.group) === null || _f === void 0 ? void 0 : _f.__typename);
|
|
130
|
+
const label = `${name} (${type})`;
|
|
131
|
+
return {
|
|
132
|
+
name: label,
|
|
133
|
+
value: {
|
|
134
|
+
name: label,
|
|
135
|
+
id: id,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
case "AppNotFoundError":
|
|
140
|
+
x = cmd.error("App not found");
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
cmd.error("Unknown error occurred.");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error instanceof Error || typeof error === "string") {
|
|
148
|
+
cmd.error(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Helper functions
|
|
153
|
+
async function selectRequestableItems(cmd, client, requestMap) {
|
|
154
|
+
const { App } = await inquirer.prompt([
|
|
155
|
+
{
|
|
156
|
+
name: "App",
|
|
157
|
+
message: "Select an app:",
|
|
158
|
+
type: "autocomplete",
|
|
159
|
+
source: async (answers, input) => {
|
|
160
|
+
var _a;
|
|
161
|
+
return (_a = (await queryRequestableApps(cmd, client, input))) !== null && _a !== void 0 ? _a : [];
|
|
162
|
+
},
|
|
163
|
+
pageSize: 15,
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
// Set the app in the requestMap and call choose assets step
|
|
167
|
+
if (!requestMap.has(App.id)) {
|
|
168
|
+
requestMap.set(App.id, {
|
|
169
|
+
appName: App.name,
|
|
170
|
+
assets: new Map(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
await chooseAssets(cmd, client, App.id, requestMap);
|
|
174
|
+
}
|
|
175
|
+
async function chooseAssets(cmd, client, appId, requestMap) {
|
|
176
|
+
var _a;
|
|
177
|
+
const { Assets } = await inquirer.prompt({
|
|
178
|
+
name: "Assets",
|
|
179
|
+
type: "checkbox",
|
|
180
|
+
pageSize: 15,
|
|
181
|
+
message: "Select one or more items:",
|
|
182
|
+
choices: (_a = (await queryRequestableAssets(cmd, client, appId, undefined))) !== null && _a !== void 0 ? _a : [],
|
|
183
|
+
validate: (answer) => {
|
|
184
|
+
if (answer.length < 1) {
|
|
185
|
+
return "You must select at least one item.";
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const entry = requestMap.get(appId);
|
|
191
|
+
for (const asset of Assets) {
|
|
192
|
+
if (entry === undefined) {
|
|
193
|
+
throw new Error(`App ${appId} not found in requestMap`);
|
|
194
|
+
}
|
|
195
|
+
if (!entry.assets.has(asset.id)) {
|
|
196
|
+
entry.assets.set(asset.id, {
|
|
197
|
+
assetName: asset.name,
|
|
198
|
+
roles: new Map(),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
await chooseRoles(appId, asset.id, requestMap);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function chooseRoles(appId, assetId, requestMap) {
|
|
205
|
+
var _a;
|
|
206
|
+
const { roles } = await inquirer.prompt({
|
|
207
|
+
name: "roles",
|
|
208
|
+
type: "checkbox",
|
|
209
|
+
message: `Select one or more roles for ${assetId}:`,
|
|
210
|
+
choices: ["push", "pull", "triage", "admin"],
|
|
211
|
+
});
|
|
212
|
+
const entry = requestMap.get(appId);
|
|
213
|
+
const assetEntry = entry === null || entry === void 0 ? void 0 : entry.assets.get(assetId);
|
|
214
|
+
if (entry === undefined || assetEntry === undefined) {
|
|
215
|
+
throw new Error(`App ${appId} or Asset ${assetId} not found in requestMap`);
|
|
216
|
+
}
|
|
217
|
+
for (const role of roles) {
|
|
218
|
+
(_a = assetEntry.roles) === null || _a === void 0 ? void 0 : _a.set(role, {
|
|
219
|
+
roleName: role,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function doneSelectingAssets() {
|
|
224
|
+
const submitMessage = "✅ Yes, proceed with request";
|
|
225
|
+
const addMoreMessage = "❌ No, add more items";
|
|
226
|
+
const { submitOrAdd } = await inquirer.prompt([
|
|
227
|
+
{
|
|
228
|
+
name: "submitOrAdd",
|
|
229
|
+
message: "Is this all you want to request?",
|
|
230
|
+
type: "list",
|
|
231
|
+
choices: [submitMessage, addMoreMessage],
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
return submitOrAdd === submitMessage;
|
|
235
|
+
}
|
|
236
|
+
async function promptForReason() {
|
|
237
|
+
return await inquirer.prompt([
|
|
238
|
+
{
|
|
239
|
+
name: "reason",
|
|
240
|
+
message: "I need access to this because...",
|
|
241
|
+
type: "input",
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
async function promptForExpiration() {
|
|
246
|
+
return await inquirer.prompt([
|
|
247
|
+
{
|
|
248
|
+
name: "expiration",
|
|
249
|
+
message: "When should access expire?",
|
|
250
|
+
type: "list",
|
|
251
|
+
choices: ["1 hour", "1 day", "7 days", "30 days", "1 year", "Indefinite"],
|
|
252
|
+
},
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
255
|
+
async function submitFinalRequest(cmd) {
|
|
256
|
+
const submitMessage = "✅ Yes, submit request";
|
|
257
|
+
const cancelMessage = "❌ No, cancel request";
|
|
258
|
+
const { submit } = await inquirer.prompt([
|
|
259
|
+
{
|
|
260
|
+
name: "submit",
|
|
261
|
+
message: "Submit request?",
|
|
262
|
+
type: "list",
|
|
263
|
+
choices: [submitMessage, cancelMessage],
|
|
264
|
+
},
|
|
265
|
+
]);
|
|
266
|
+
if (submit === submitMessage) {
|
|
267
|
+
const requestLink = "https://dev.opal.dev/requests/sent/05ca5d5f-ea60-4cdb-84e1-7e3c575b2b72"; //TODO: Replace with actual request link
|
|
268
|
+
cmd.log("\n🎉 Your Access Request has been submitted! Request ID: 1234");
|
|
269
|
+
cmd.log(`🔍 View request status here: ${requestLink}`);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
cmd.log("🚫 Access Request has been cancelled.");
|
|
273
|
+
}
|
|
274
|
+
}
|
package/lib/lib/resources.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { ResourceAccessLevel, ResourceAccessLevelInput } from
|
|
1
|
+
import type { Command } from "@oclif/core";
|
|
2
|
+
import type { ResourceAccessLevel, ResourceAccessLevelInput } from "../types";
|
|
3
3
|
export type ResourceInfo = {
|
|
4
4
|
id: string;
|
|
5
5
|
name: string;
|
package/lib/lib/resources.js
CHANGED
|
@@ -5,16 +5,15 @@ const inquirer = require("inquirer");
|
|
|
5
5
|
const handler_1 = require("../handler");
|
|
6
6
|
const apollo_1 = require("./apollo");
|
|
7
7
|
exports.DEFAULT_ACCESS_LEVEL = {
|
|
8
|
-
accessLevelName:
|
|
9
|
-
accessLevelRemoteId:
|
|
8
|
+
accessLevelName: "",
|
|
9
|
+
accessLevelRemoteId: "",
|
|
10
10
|
};
|
|
11
11
|
// Returns a filtered array of items that match the input
|
|
12
12
|
const filterChoices = (input, choices) => {
|
|
13
|
-
|
|
14
|
-
if (input === '') {
|
|
13
|
+
if (!input) {
|
|
15
14
|
return choices;
|
|
16
15
|
}
|
|
17
|
-
return choices.filter(choice => choice.name.toLowerCase().includes(input.toLowerCase()));
|
|
16
|
+
return choices.filter((choice) => choice.name.toLowerCase().includes(input.toLowerCase()));
|
|
18
17
|
};
|
|
19
18
|
exports.filterChoices = filterChoices;
|
|
20
19
|
const ListResourcesDocumentTemplate = `
|
|
@@ -37,8 +36,8 @@ query ListResources {
|
|
|
37
36
|
}
|
|
38
37
|
}`;
|
|
39
38
|
const promptUserForResource = async (command, resourceType, message) => {
|
|
40
|
-
const listResourcesDocument = ListResourcesDocumentTemplate.replace(
|
|
41
|
-
const { resp, error } = await (0, handler_1.
|
|
39
|
+
const listResourcesDocument = ListResourcesDocumentTemplate.replace("RESOURCE_TYPE", resourceType);
|
|
40
|
+
const { resp, error } = await (0, handler_1.runQueryDeprecated)({
|
|
42
41
|
command: command,
|
|
43
42
|
query: listResourcesDocument,
|
|
44
43
|
variables: {},
|
|
@@ -60,19 +59,22 @@ const promptUserForResource = async (command, resourceType, message) => {
|
|
|
60
59
|
};
|
|
61
60
|
});
|
|
62
61
|
if (resourceInfos.length === 0) {
|
|
63
|
-
return (0, apollo_1.handleError)(command,
|
|
62
|
+
return (0, apollo_1.handleError)(command, "You don't have access to any resources of this type. Please go to Opal to request access.");
|
|
64
63
|
}
|
|
65
64
|
const resourceInfoByName = {};
|
|
66
|
-
|
|
65
|
+
// biome-ignore lint/complexity/noForEach: fix it when you get the chance
|
|
66
|
+
resourceInfos.forEach((resourceInfo) => {
|
|
67
67
|
resourceInfoByName[resourceInfo.name] = resourceInfo;
|
|
68
68
|
});
|
|
69
|
-
inquirer.registerPrompt(
|
|
70
|
-
const selectedResourceInfo = await inquirer.prompt([
|
|
71
|
-
|
|
69
|
+
inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt"));
|
|
70
|
+
const selectedResourceInfo = await inquirer.prompt([
|
|
71
|
+
{
|
|
72
|
+
name: "resource",
|
|
72
73
|
message: message,
|
|
73
|
-
type:
|
|
74
|
+
type: "autocomplete",
|
|
74
75
|
source: (answers, input) => (0, exports.filterChoices)(input, resourceInfos),
|
|
75
|
-
}
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
76
78
|
return resourceInfoByName[selectedResourceInfo.resource];
|
|
77
79
|
};
|
|
78
80
|
exports.promptUserForResource = promptUserForResource;
|
|
@@ -90,7 +92,7 @@ query ListAccessLevelsForResource($resourceId: ResourceId!) {
|
|
|
90
92
|
}`;
|
|
91
93
|
const promptUserForAccessLevels = async (command, resourceId, instanceType, accessLevelRemoteId) => {
|
|
92
94
|
var _a, _b, _c;
|
|
93
|
-
const { resp, error } = await (0, handler_1.
|
|
95
|
+
const { resp, error } = await (0, handler_1.runQueryDeprecated)({
|
|
94
96
|
command: command,
|
|
95
97
|
query: ListAccessLevelsForResource,
|
|
96
98
|
variables: { resourceId },
|
|
@@ -103,11 +105,12 @@ const promptUserForAccessLevels = async (command, resourceId, instanceType, acce
|
|
|
103
105
|
name: resp.accessLevelName,
|
|
104
106
|
}));
|
|
105
107
|
if (!accessLevelInfos) {
|
|
106
|
-
return (0, apollo_1.handleError)(command,
|
|
108
|
+
return (0, apollo_1.handleError)(command, "This resource requires an access level, but none have been set up in Opal. Please contact your Opal admin.");
|
|
107
109
|
}
|
|
108
110
|
const accessLevelInfoByName = {};
|
|
109
111
|
const accessLevelInfoByRemoteId = {};
|
|
110
|
-
|
|
112
|
+
// biome-ignore lint/complexity/noForEach: please fix this
|
|
113
|
+
accessLevelInfos.forEach((accessLevelInfo) => {
|
|
111
114
|
accessLevelInfoByName[accessLevelInfo.name] = accessLevelInfo;
|
|
112
115
|
accessLevelInfoByRemoteId[accessLevelInfo.id] = accessLevelInfo;
|
|
113
116
|
});
|
|
@@ -117,14 +120,17 @@ const promptUserForAccessLevels = async (command, resourceId, instanceType, acce
|
|
|
117
120
|
}
|
|
118
121
|
// Prompt user to pick access levels available to them
|
|
119
122
|
if (!selectedAccessLevel) {
|
|
120
|
-
inquirer.registerPrompt(
|
|
121
|
-
const selectedAccessLevelInfo = await inquirer.prompt([
|
|
122
|
-
|
|
123
|
+
inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt"));
|
|
124
|
+
const selectedAccessLevelInfo = await inquirer.prompt([
|
|
125
|
+
{
|
|
126
|
+
name: "accessLevel",
|
|
123
127
|
message: `Select an access level to the ${instanceType}`,
|
|
124
|
-
type:
|
|
128
|
+
type: "autocomplete",
|
|
125
129
|
source: (answers, input) => (0, exports.filterChoices)(input, accessLevelInfos),
|
|
126
|
-
}
|
|
127
|
-
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
selectedAccessLevel =
|
|
133
|
+
accessLevelInfoByName[selectedAccessLevelInfo.accessLevel];
|
|
128
134
|
}
|
|
129
135
|
return {
|
|
130
136
|
accessLevelName: selectedAccessLevel.name,
|
package/lib/lib/sessions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { ResourceAccessLevelInput } from
|
|
1
|
+
import type { Command } from "@oclif/core";
|
|
2
|
+
import type { ResourceAccessLevelInput } from "../types";
|
|
3
3
|
export declare const getOrCreateSession: (command: Command, resourceId: string, accessLevel: ResourceAccessLevelInput, sessionId: string | undefined, metadataFragment: string, wantNewSession?: boolean) => Promise<any>;
|
|
4
4
|
export declare const getSessionExpirationMessage: (session: any) => string;
|
package/lib/lib/sessions.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getSessionExpirationMessage = exports.getOrCreateSession = void 0;
|
|
4
|
+
const moment = require("moment");
|
|
4
5
|
const open = require("open");
|
|
5
|
-
const config_1 = require("../lib/config");
|
|
6
6
|
const handler_1 = require("../handler");
|
|
7
|
-
const moment = require("moment");
|
|
8
7
|
const apollo_1 = require("../lib/apollo");
|
|
8
|
+
const config_1 = require("../lib/config");
|
|
9
9
|
const util_1 = require("./util");
|
|
10
10
|
const CreateSessionDocument = `
|
|
11
11
|
mutation CreateSession($id: ResourceId!, $accessLevel: ResourceAccessLevelInput!, $sessionId: SessionId) {
|
|
@@ -59,9 +59,9 @@ query ListSessions($id: ResourceId!) {
|
|
|
59
59
|
}
|
|
60
60
|
`;
|
|
61
61
|
const getSession = async (command, resourceId, accessLevelRemoteId, sessionId, metadataFragment, minCreatedAt) => {
|
|
62
|
-
const { resp, error } = await (0, handler_1.
|
|
62
|
+
const { resp, error } = await (0, handler_1.runQueryDeprecated)({
|
|
63
63
|
command: command,
|
|
64
|
-
query: ListSessionsDocument.replace(
|
|
64
|
+
query: ListSessionsDocument.replace("METADATA_FRAGMENT", metadataFragment),
|
|
65
65
|
variables: {
|
|
66
66
|
id: resourceId,
|
|
67
67
|
},
|
|
@@ -70,8 +70,9 @@ const getSession = async (command, resourceId, accessLevelRemoteId, sessionId, m
|
|
|
70
70
|
return (0, apollo_1.handleError)(command, error);
|
|
71
71
|
}
|
|
72
72
|
switch (resp === null || resp === void 0 ? void 0 : resp.data.sessions.__typename) {
|
|
73
|
-
case
|
|
73
|
+
case "SessionsResult": {
|
|
74
74
|
const sessions = resp.data.sessions.sessions;
|
|
75
|
+
// biome-ignore lint/suspicious/noImplicitAnyLet: please fix how we do queries in this cli
|
|
75
76
|
let selectedSession;
|
|
76
77
|
for (const session of sessions) {
|
|
77
78
|
if (sessionId && session.id !== sessionId) {
|
|
@@ -84,7 +85,8 @@ const getSession = async (command, resourceId, accessLevelRemoteId, sessionId, m
|
|
|
84
85
|
// This lets us wait until we get a session that's newly created
|
|
85
86
|
continue;
|
|
86
87
|
}
|
|
87
|
-
if (!selectedSession ||
|
|
88
|
+
if (!selectedSession ||
|
|
89
|
+
moment(session.endTime).diff(selectedSession.endTime) > 0) {
|
|
88
90
|
// Select the session with the latest end time
|
|
89
91
|
selectedSession = session;
|
|
90
92
|
}
|
|
@@ -105,13 +107,12 @@ const openBrowserAndPollForSession = async (command, message, resourceId, access
|
|
|
105
107
|
const configData = (0, config_1.getOrCreateConfigData)(command.config.configDir);
|
|
106
108
|
const url = configData[config_1.urlKey];
|
|
107
109
|
setTimeout(() => {
|
|
108
|
-
open(url
|
|
110
|
+
open(`${url}/resources/${resourceId}?showModal=true&refresh=true`);
|
|
109
111
|
}, 2000); // Wait before opening the browser to give the user time to read the message
|
|
112
|
+
// biome-ignore lint/suspicious/noImplicitAnyLet: please fix typing for queries
|
|
110
113
|
let session;
|
|
111
114
|
while (!session) {
|
|
112
|
-
// eslint-disable-next-line no-await-in-loop
|
|
113
115
|
await (0, util_1.sleep)(2000);
|
|
114
|
-
// eslint-disable-next-line no-await-in-loop
|
|
115
116
|
session = await getSession(command, resourceId, accessLevelRemoteId, sessionId, metadataFragment, minCreatedAt);
|
|
116
117
|
}
|
|
117
118
|
return session;
|
|
@@ -119,7 +120,7 @@ const openBrowserAndPollForSession = async (command, message, resourceId, access
|
|
|
119
120
|
const createSession = async (command, resourceId, accessLevel, sessionId, metadataFragment, wantNewSession) => {
|
|
120
121
|
const { resp, error } = await (0, handler_1.runMutation)({
|
|
121
122
|
command: command,
|
|
122
|
-
query: CreateSessionDocument.replace(
|
|
123
|
+
query: CreateSessionDocument.replace("METADATA_FRAGMENT", metadataFragment),
|
|
123
124
|
variables: {
|
|
124
125
|
id: resourceId,
|
|
125
126
|
accessLevel: accessLevel,
|
|
@@ -130,14 +131,14 @@ const createSession = async (command, resourceId, accessLevel, sessionId, metada
|
|
|
130
131
|
return (0, apollo_1.handleError)(command, error);
|
|
131
132
|
}
|
|
132
133
|
switch (resp === null || resp === void 0 ? void 0 : resp.data.createSession.__typename) {
|
|
133
|
-
case
|
|
134
|
+
case "CreateSessionResult": {
|
|
134
135
|
return resp.data.createSession.session;
|
|
135
136
|
}
|
|
136
|
-
case
|
|
137
|
-
return openBrowserAndPollForSession(command,
|
|
137
|
+
case "MfaInvalidError": {
|
|
138
|
+
return openBrowserAndPollForSession(command, "❗ MFA validation needed. Please connect via browser. Opening browser and awaiting validation...", resourceId, accessLevel.accessLevelRemoteId, sessionId, metadataFragment, wantNewSession);
|
|
138
139
|
}
|
|
139
|
-
case
|
|
140
|
-
return openBrowserAndPollForSession(command,
|
|
140
|
+
case "OidcIDTokenNotFoundError": {
|
|
141
|
+
return openBrowserAndPollForSession(command, "❗ OIDC authentication needed. Please connect via browser. Opening browser and awaiting authentication...", resourceId, accessLevel.accessLevelRemoteId, sessionId, metadataFragment, wantNewSession);
|
|
141
142
|
}
|
|
142
143
|
default:
|
|
143
144
|
return (0, apollo_1.handleError)(command, error, resp);
|
|
@@ -152,14 +153,14 @@ const getOrCreateSession = async (command, resourceId, accessLevel, sessionId, m
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
if (sessionId) {
|
|
155
|
-
return (0, apollo_1.handleError)(command,
|
|
156
|
+
return (0, apollo_1.handleError)(command, `Session not found for given id: ${sessionId}`);
|
|
156
157
|
}
|
|
157
158
|
// Create new session
|
|
158
159
|
return createSession(command, resourceId, accessLevel, sessionId, metadataFragment, wantNewSession);
|
|
159
160
|
};
|
|
160
161
|
exports.getOrCreateSession = getOrCreateSession;
|
|
161
162
|
const getSessionExpirationMessage = (session) => {
|
|
162
|
-
const diff = moment(session.endTime).diff(moment(),
|
|
163
|
+
const diff = moment(session.endTime).diff(moment(), "minutes");
|
|
163
164
|
const hours = Math.floor(diff / 60);
|
|
164
165
|
const minutes = diff % 60;
|
|
165
166
|
return `${hours}h ${minutes}m`;
|