opal-security 2.3.4 → 3.0.0

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 CHANGED
@@ -22,7 +22,7 @@ $ npm install -g opal-security
22
22
  $ opal COMMAND
23
23
  running command...
24
24
  $ opal (--version)
25
- opal-security/2.3.4 darwin-arm64 node-v18.19.0
25
+ opal-security/3.0.0 darwin-arm64 node-v22.14.0
26
26
  $ opal --help [COMMAND]
27
27
  USAGE
28
28
  $ opal COMMAND
@@ -35,15 +35,16 @@ USAGE
35
35
  <!-- commands -->
36
36
  * [`opal autocomplete [SHELL]`](#opal-autocomplete-shell)
37
37
  * [`opal aws:identity`](#opal-awsidentity)
38
+ * [`opal clear-auth-provider`](#opal-clear-auth-provider)
38
39
  * [`opal curl-example`](#opal-curl-example)
39
40
  * [`opal help [COMMANDS]`](#opal-help-commands)
40
41
  * [`opal iam-roles:start`](#opal-iam-rolesstart)
41
42
  * [`opal kube-roles:start`](#opal-kube-rolesstart)
42
43
  * [`opal login`](#opal-login)
43
44
  * [`opal logout`](#opal-logout)
44
- * [`opal migrate-creds`](#opal-migrate-creds)
45
45
  * [`opal postgres-instances:start`](#opal-postgres-instancesstart)
46
46
  * [`opal resources:get`](#opal-resourcesget)
47
+ * [`opal set-auth-provider`](#opal-set-auth-provider)
47
48
  * [`opal set-custom-header`](#opal-set-custom-header)
48
49
  * [`opal set-token`](#opal-set-token)
49
50
  * [`opal set-url [URL]`](#opal-set-url-url)
@@ -99,7 +100,27 @@ EXAMPLES
99
100
  $ opal aws:identity
100
101
  ```
101
102
 
102
- _See code: [src/commands/aws/identity.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/aws/identity.ts)_
103
+ _See code: [src/commands/aws/identity.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/aws/identity.ts)_
104
+
105
+ ## `opal clear-auth-provider`
106
+
107
+ Clears the custom Issuer URL and Client ID set by set-airgap-auth, returning to the default.
108
+
109
+ ```
110
+ USAGE
111
+ $ opal clear-auth-provider [-h]
112
+
113
+ FLAGS
114
+ -h, --help Show CLI help.
115
+
116
+ DESCRIPTION
117
+ Clears the custom Issuer URL and Client ID set by set-airgap-auth, returning to the default.
118
+
119
+ EXAMPLES
120
+ $ opal clear-auth-provider
121
+ ```
122
+
123
+ _See code: [src/commands/clear-auth-provider.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/clear-auth-provider.ts)_
103
124
 
104
125
  ## `opal curl-example`
105
126
 
@@ -116,7 +137,7 @@ DESCRIPTION
116
137
  Prints out an example cURL command containing the parameters the CLI uses to query the Opal server.
117
138
  ```
118
139
 
119
- _See code: [src/commands/curl-example.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/curl-example.ts)_
140
+ _See code: [src/commands/curl-example.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/curl-example.ts)_
120
141
 
121
142
  ## `opal help [COMMANDS]`
122
143
 
@@ -124,10 +145,10 @@ Display help for opal.
124
145
 
125
146
  ```
126
147
  USAGE
127
- $ opal help [COMMANDS] [-n]
148
+ $ opal help [COMMANDS...] [-n]
128
149
 
129
150
  ARGUMENTS
130
- COMMANDS Command to show help for.
151
+ COMMANDS... Command to show help for.
131
152
 
132
153
  FLAGS
133
154
  -n, --nested-commands Include all nested commands in the output.
@@ -166,7 +187,7 @@ EXAMPLES
166
187
  $ opal iam-roles:start --id 51f7176b-0464-4a6f-8369-e951e187b398 --profileName "custom-profile"
167
188
  ```
168
189
 
169
- _See code: [src/commands/iam-roles/start.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/iam-roles/start.ts)_
190
+ _See code: [src/commands/iam-roles/start.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/iam-roles/start.ts)_
170
191
 
171
192
  ## `opal kube-roles:start`
172
193
 
@@ -197,7 +218,7 @@ EXAMPLES
197
218
  $ opal kube-roles:start --id 51f7176b-0464-4a6f-8369-e951e187b398 --accessLevelRemoteId "arn:aws:iam::712234975475:role/acme-eks-cluster-admin-role"
198
219
  ```
199
220
 
200
- _See code: [src/commands/kube-roles/start.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/kube-roles/start.ts)_
221
+ _See code: [src/commands/kube-roles/start.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/kube-roles/start.ts)_
201
222
 
202
223
  ## `opal login`
203
224
 
@@ -218,7 +239,7 @@ EXAMPLES
218
239
  $ opal login
219
240
  ```
220
241
 
221
- _See code: [src/commands/login.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/login.ts)_
242
+ _See code: [src/commands/login.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/login.ts)_
222
243
 
223
244
  ## `opal logout`
224
245
 
@@ -238,24 +259,7 @@ EXAMPLES
238
259
  $ opal logout
239
260
  ```
240
261
 
241
- _See code: [src/commands/logout.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/logout.ts)_
242
-
243
- ## `opal migrate-creds`
244
-
245
- Migrates credentials from old keystore to new store. Should only need to be run once
246
-
247
- ```
248
- USAGE
249
- $ opal migrate-creds [-h]
250
-
251
- FLAGS
252
- -h, --help Show CLI help.
253
-
254
- DESCRIPTION
255
- Migrates credentials from old keystore to new store. Should only need to be run once
256
- ```
257
-
258
- _See code: [src/commands/migrate-creds.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/migrate-creds.ts)_
262
+ _See code: [src/commands/logout.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/logout.ts)_
259
263
 
260
264
  ## `opal postgres-instances:start`
261
265
 
@@ -293,7 +297,7 @@ EXAMPLES
293
297
  $ opal postgres-instances:start --id 51f7176b-0464-4a6f-8369-e951e187b398 --accessLevelRemoteId fullaccess --action view
294
298
  ```
295
299
 
296
- _See code: [src/commands/postgres-instances/start.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/postgres-instances/start.ts)_
300
+ _See code: [src/commands/postgres-instances/start.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/postgres-instances/start.ts)_
297
301
 
298
302
  ## `opal resources:get`
299
303
 
@@ -314,7 +318,33 @@ EXAMPLES
314
318
  $ opal resources:get --id 54052a3e-5375-4392-aeaf-0c6c44c131d4
315
319
  ```
316
320
 
317
- _See code: [src/commands/resources/get.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/resources/get.ts)_
321
+ _See code: [src/commands/resources/get.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/resources/get.ts)_
322
+
323
+ ## `opal set-auth-provider`
324
+
325
+ Sets the Issuer URL and Client ID of the Auth Provider that the CLI will authenticate with.
326
+
327
+ ```
328
+ USAGE
329
+ $ opal set-auth-provider --clientID <value> --issuerUrl <value> [-h]
330
+
331
+ FLAGS
332
+ -h, --help Show CLI help.
333
+ --clientID=<value> (required) Client ID of your Auth Provider
334
+ --issuerUrl=<value> (required) Issuer URL of your Auth Provider
335
+
336
+ DESCRIPTION
337
+ Sets the Issuer URL and Client ID of the Auth Provider that the CLI will authenticate with.
338
+ Only use this if you are running a self-hosted, air-gapped instance of Opal that uses a custom Auth Provider.
339
+
340
+ Note - you will need an OIDC provider that supports the device_code grant.
341
+
342
+
343
+ EXAMPLES
344
+ $ opal set-auth-provider --clientID 1234asdf --issuerUrl https://auth.example.com
345
+ ```
346
+
347
+ _See code: [src/commands/set-auth-provider.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/set-auth-provider.ts)_
318
348
 
319
349
  ## `opal set-custom-header`
320
350
 
@@ -335,7 +365,7 @@ EXAMPLES
335
365
  $ opal set-custom-header --header 'cf-access-token: $TOKEN'
336
366
  ```
337
367
 
338
- _See code: [src/commands/set-custom-header.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/set-custom-header.ts)_
368
+ _See code: [src/commands/set-custom-header.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/set-custom-header.ts)_
339
369
 
340
370
  ## `opal set-token`
341
371
 
@@ -355,7 +385,7 @@ EXAMPLES
355
385
  $ opal set-token
356
386
  ```
357
387
 
358
- _See code: [src/commands/set-token.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/set-token.ts)_
388
+ _See code: [src/commands/set-token.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/set-token.ts)_
359
389
 
360
390
  ## `opal set-url [URL]`
361
391
 
@@ -379,7 +409,7 @@ EXAMPLES
379
409
  $ opal set-url
380
410
  ```
381
411
 
382
- _See code: [src/commands/set-url.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/set-url.ts)_
412
+ _See code: [src/commands/set-url.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/set-url.ts)_
383
413
 
384
414
  ## `opal ssh:copyFrom`
385
415
 
@@ -410,7 +440,7 @@ EXAMPLES
410
440
  $ opal ssh:copyFrom --src instance/dir --dest my/dir --id 51f7176b-0464-4a6f-8369-e951e187b398
411
441
  ```
412
442
 
413
- _See code: [src/commands/ssh/copyFrom.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/ssh/copyFrom.ts)_
443
+ _See code: [src/commands/ssh/copyFrom.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/ssh/copyFrom.ts)_
414
444
 
415
445
  ## `opal ssh:copyTo`
416
446
 
@@ -441,7 +471,7 @@ EXAMPLES
441
471
  $ opal ssh:copyTo --src my/dir --dest instance/dir --id 51f7176b-0464-4a6f-8369-e951e187b398
442
472
  ```
443
473
 
444
- _See code: [src/commands/ssh/copyTo.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/ssh/copyTo.ts)_
474
+ _See code: [src/commands/ssh/copyTo.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/ssh/copyTo.ts)_
445
475
 
446
476
  ## `opal ssh:start`
447
477
 
@@ -468,7 +498,7 @@ EXAMPLES
468
498
  $ opal ssh:start --id 51f7176b-0464-4a6f-8369-e951e187b398
469
499
  ```
470
500
 
471
- _See code: [src/commands/ssh/start.ts](https://github.com/opalsecurity/opal-cli/blob/v2.3.4/src/commands/ssh/start.ts)_
501
+ _See code: [src/commands/ssh/start.ts](https://github.com/opalsecurity/opal-cli/blob/v3.0.0/src/commands/ssh/start.ts)_
472
502
 
473
503
  ## `opal version`
474
504
 
@@ -1,6 +1,7 @@
1
1
  import { Command } from '@oclif/core';
2
- export default class MigrateCreds extends Command {
2
+ export default class ClearAuthProvider extends Command {
3
3
  static description: string;
4
+ static examples: string[];
4
5
  static flags: {
5
6
  help: import("@oclif/core/lib/interfaces").BooleanFlag<void>;
6
7
  };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const config_1 = require("../lib/config");
5
+ const credentials_1 = require("../lib/credentials");
6
+ const flags_1 = require("../lib/flags");
7
+ class ClearAuthProvider extends core_1.Command {
8
+ async run() {
9
+ try {
10
+ const { flags, args } = await this.parse(ClearAuthProvider);
11
+ const configData = (0, config_1.getOrCreateConfigData)(this.config.configDir);
12
+ configData.issuerURL = null;
13
+ configData.clientID = null;
14
+ (0, config_1.writeConfigData)(this.config.configDir, configData);
15
+ await (0, credentials_1.removeOpalCredentials)(this);
16
+ this.log('Client ID and Issuer URL reset to defaults');
17
+ }
18
+ catch (error) {
19
+ this.error(error);
20
+ }
21
+ }
22
+ }
23
+ ClearAuthProvider.description = `Clears the custom Issuer URL and Client ID set by set-airgap-auth, returning to the default.`;
24
+ ClearAuthProvider.examples = ['$ opal clear-auth-provider'];
25
+ ClearAuthProvider.flags = {
26
+ help: flags_1.SHARED_FLAGS.help,
27
+ };
28
+ exports.default = ClearAuthProvider;
@@ -7,15 +7,22 @@ const flags_1 = require("../lib/flags");
7
7
  class CurlExample extends core_1.Command {
8
8
  async run() {
9
9
  const opalCreds = await (0, credentials_1.getOpalCredentials)(this);
10
- const accessToken = opalCreds === null || opalCreds === void 0 ? void 0 : opalCreds.accessToken;
10
+ const secret = opalCreds === null || opalCreds === void 0 ? void 0 : opalCreds.secret;
11
11
  const organizationID = opalCreds === null || opalCreds === void 0 ? void 0 : opalCreds.organizationID;
12
12
  const configData = (0, config_1.getOrCreateConfigData)(this.config.configDir);
13
13
  const url = configData[config_1.urlKey];
14
+ let authStr = '';
15
+ if (opalCreds.secretType === credentials_1.SecretType.ApiToken) {
16
+ authStr = `Authorization: Bearer ${secret}`;
17
+ }
18
+ else {
19
+ authStr = `Cookie: ${secret}`;
20
+ }
14
21
  this.log(`
15
22
  curl -v ${url}/query \\
16
23
  --data-binary '{"query":"query ListSSHSessions {resources(input: {serviceType: SSH, onlyMine: true}) {... on ResourcesResult { resources { name } } } }"}' \\
17
24
  --header "Content-Type: application/json" \\
18
- --header "Authorization: Bearer ${accessToken}" \\
25
+ --header "${authStr}" \\
19
26
  --header "X-Opal-Organization-ID: ${organizationID}"
20
27
  `);
21
28
  }
@@ -2,6 +2,7 @@ import { Command } from '@oclif/core';
2
2
  export declare const CLISignInMethodName = "CLISignInMethod";
3
3
  export declare const CLIAuthSessionCheckName = "CLIAuthSessionCheck";
4
4
  export declare const CLIAuthSessionCheckDocument = "\nquery CLIAuthSessionCheck {\n organizationSettings {\n ... on OrganizationSettingsResult {\n settings {\n id\n }\n }\n }\n}\n";
5
+ export declare const CLITokenExchangeName = "CLITokenExchange";
5
6
  export default class Login extends Command {
6
7
  static description: string;
7
8
  static examples: string[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CLIAuthSessionCheckDocument = exports.CLIAuthSessionCheckName = exports.CLISignInMethodName = void 0;
3
+ exports.CLITokenExchangeName = exports.CLIAuthSessionCheckDocument = exports.CLIAuthSessionCheckName = exports.CLISignInMethodName = void 0;
4
4
  const core_1 = require("@oclif/core");
5
5
  const open = require("open");
6
6
  const openid_client_1 = require("openid-client");
@@ -54,9 +54,33 @@ query CLIAuthSessionCheck {
54
54
  }
55
55
  }
56
56
  `;
57
+ const SignInDocument = `
58
+ mutation SignIn($input: SignInInput!) {
59
+ signIn(input: $input) {
60
+ __typename
61
+ ... on SignInResult {
62
+ state
63
+ forceExtraStep
64
+ authURL
65
+ __typename
66
+ }
67
+ }
68
+ }
69
+ `;
70
+ exports.CLITokenExchangeName = 'CLITokenExchange';
71
+ const CLITokenExchangeDocument = `
72
+ mutation CLITokenExchange($input: CLITokenExchangeInput!) {
73
+ cliTokenExchange(input: $input) {
74
+ __typename
75
+ ... on CLITokenExchangeOutput {
76
+ sessionID
77
+ }
78
+ }
79
+ }
80
+ `;
57
81
  class Login extends core_1.Command {
58
82
  async run() {
59
- var _a, _b, _c, _d, _e, _f, _g;
83
+ var _a, _b, _c, _d, _e, _f, _g, _h;
60
84
  try {
61
85
  await (0, apollo_1.initClient)(this, false);
62
86
  const { flags } = await this.parse(Login);
@@ -64,13 +88,13 @@ class Login extends core_1.Command {
64
88
  const configData = (0, config_1.getOrCreateConfigData)(configDir);
65
89
  let email = flags.email;
66
90
  let organizationID;
67
- let clientIdCandidate;
91
+ let clientIDCandidate;
68
92
  const existingCreds = await (0, credentials_1.getOpalCredentials)(this, false);
69
93
  // Only use the previous email + organizationID if email isn't explicitly specified.
70
94
  if (!email) {
71
95
  email = existingCreds.email;
72
96
  organizationID = existingCreds.organizationID;
73
- clientIdCandidate = existingCreds.clientIDCandidate;
97
+ clientIDCandidate = existingCreds.clientIDCandidate;
74
98
  }
75
99
  await (0, credentials_1.removeOpalCredentials)(this);
76
100
  this.log('Welcome to Opal! ⚡️\n');
@@ -118,7 +142,7 @@ class Login extends core_1.Command {
118
142
  if (signInOrganizations && signInOrganizations.length > 0) {
119
143
  if (signInOrganizations.length === 1) {
120
144
  organizationID = signInOrganizations[0].organizationId;
121
- clientIdCandidate = signInOrganizations[0].cliClientId;
145
+ clientIDCandidate = signInOrganizations[0].cliClientId;
122
146
  }
123
147
  else {
124
148
  const responses = await inquirer.prompt([{
@@ -131,7 +155,7 @@ class Login extends core_1.Command {
131
155
  })),
132
156
  }]);
133
157
  organizationID = responses.signInOrganization.organizationId;
134
- clientIdCandidate = responses.signInOrganization.cliClientId;
158
+ clientIDCandidate = responses.signInOrganization.cliClientId;
135
159
  }
136
160
  }
137
161
  else {
@@ -139,27 +163,44 @@ class Login extends core_1.Command {
139
163
  // which is parity with our web app.
140
164
  }
141
165
  }
166
+ const { resp: signInResp } = await (0, handler_1.runMutation)({
167
+ command: this,
168
+ query: SignInDocument,
169
+ variables: {
170
+ input: { organizationId: organizationID },
171
+ },
172
+ });
173
+ const state = (_e = signInResp === null || signInResp === void 0 ? void 0 : signInResp.data.signIn) === null || _e === void 0 ? void 0 : _e.state;
142
174
  let issuer;
143
- if ((0, config_1.isProduction)(this.config.configDir)) {
175
+ // issuerURL may come from configData if set by set-airgap-auth
176
+ if (!!configData.issuerURL) {
177
+ issuer = await openid_client_1.Issuer.discover(configData.issuerURL);
178
+ }
179
+ else if ((0, config_1.isProduction)(this.config.configDir)) {
144
180
  issuer = await openid_client_1.Issuer.discover(ISSUER_PROD);
145
181
  }
146
182
  else {
147
183
  issuer = await openid_client_1.Issuer.discover(ISSUER_DEV);
148
184
  }
149
- let clientId;
150
- if (clientIdCandidate) {
151
- clientId = clientIdCandidate;
185
+ let clientID;
186
+ if (clientIDCandidate) {
187
+ // clientIdCandidate gets stored in creds, and is mostly relevant for on-prem envs using Auth0 and SAML
188
+ clientID = clientIDCandidate;
189
+ }
190
+ else if (!!configData.clientID) {
191
+ // clientID may come from configData if set by set-airgap-auth
192
+ clientID = configData.clientID;
152
193
  }
153
194
  else if ((0, config_1.isProduction)(this.config.configDir)) {
154
- clientId = CLIENT_ID_PROD;
195
+ clientID = CLIENT_ID_PROD;
155
196
  }
156
197
  else {
157
- clientId = CLIENT_ID_DEV;
198
+ clientID = CLIENT_ID_DEV;
158
199
  }
159
200
  /* eslint-disable camelcase */
160
201
  const client = new issuer.Client({
161
202
  grant_types: [GRANT_TYPE],
162
- client_id: clientId,
203
+ client_id: clientID,
163
204
  response_types: [],
164
205
  redirect_uris: [],
165
206
  token_endpoint_auth_method: 'none',
@@ -177,23 +218,34 @@ class Login extends core_1.Command {
177
218
  await (0, util_1.sleep)(1000);
178
219
  await open(handle.verification_uri_complete, { wait: false });
179
220
  const tokenSet = await handle.poll();
180
- const userInfo = await client.userinfo(tokenSet);
181
- let account = (userInfo.email || 'unset-email') + '|' + organizationID;
182
- if (clientIdCandidate) {
183
- // Save the clientIdCandidate only when SAML is set up for the org.
184
- account = account + '|' + clientIdCandidate;
221
+ const { error: tokenExchangeError } = await (0, handler_1.runMutation)({
222
+ command: this,
223
+ query: CLITokenExchangeDocument,
224
+ variables: {
225
+ input: {
226
+ accessToken: tokenSet === null || tokenSet === void 0 ? void 0 : tokenSet.access_token,
227
+ state,
228
+ },
229
+ },
230
+ });
231
+ if (tokenExchangeError) {
232
+ this.log("WARN: Failed to exchange access token for session in Opal. Falling back to using access token for authenticating requests\n");
233
+ // TODO: consider adding a warn line recommending upgrading Opal to version XYZ, once accompanying PR is pushed to prod
234
+ await (0, credentials_1.setOpalCredentials)(this, email, organizationID, clientIDCandidate, (tokenSet === null || tokenSet === void 0 ? void 0 : tokenSet.access_token) || '', credentials_1.SecretType.ApiToken);
235
+ }
236
+ else {
237
+ await (0, credentials_1.setOpalCredentials)(this, email, organizationID, clientIDCandidate, apollo_1.cookieStr, credentials_1.SecretType.Cookie);
185
238
  }
186
- await (0, credentials_1.setOpalCredentials)(this, userInfo.email, organizationID, clientIdCandidate, (tokenSet === null || tokenSet === void 0 ? void 0 : tokenSet.access_token) || '');
187
239
  // "Representative" authenticated call to check the log-in worked as expected.
188
240
  const { resp: authCheckResp, error: authCheckErr } = await (0, handler_1.runQuery)({
189
241
  command: this,
190
242
  query: exports.CLIAuthSessionCheckDocument,
191
243
  variables: {},
192
244
  });
193
- if (authCheckErr || !((_g = (_f = (_e = authCheckResp === null || authCheckResp === void 0 ? void 0 : authCheckResp.data) === null || _e === void 0 ? void 0 : _e.organizationSettings) === null || _f === void 0 ? void 0 : _f.settings) === null || _g === void 0 ? void 0 : _g.id)) {
245
+ if (authCheckErr || !((_h = (_g = (_f = authCheckResp === null || authCheckResp === void 0 ? void 0 : authCheckResp.data) === null || _f === void 0 ? void 0 : _f.organizationSettings) === null || _g === void 0 ? void 0 : _g.settings) === null || _h === void 0 ? void 0 : _h.id)) {
194
246
  this.log('Error verifying log in. Authenticated commands may fail. Please double check your URL and use `opal logout; opal login` to try again.\n');
195
247
  await (0, credentials_1.removeOpalCredentials)(this);
196
- (0, apollo_1.handleError)(this, authCheckErr);
248
+ process.exit(1);
197
249
  }
198
250
  this.log('🎉 You have successfully authenticated with Opal! You can now run authenticated commands.\n');
199
251
  }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SetAuthProvider extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ help: import("@oclif/core/lib/interfaces").BooleanFlag<void>;
7
+ clientID: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ issuerUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const config_1 = require("../lib/config");
5
+ const credentials_1 = require("../lib/credentials");
6
+ const flags_1 = require("../lib/flags");
7
+ class SetAuthProvider extends core_1.Command {
8
+ async run() {
9
+ try {
10
+ const { flags, args } = await this.parse(SetAuthProvider);
11
+ const configData = (0, config_1.getOrCreateConfigData)(this.config.configDir);
12
+ configData.issuerURL = flags.issuerUrl;
13
+ configData.clientID = flags.clientID;
14
+ (0, config_1.writeConfigData)(this.config.configDir, configData);
15
+ await (0, credentials_1.removeOpalCredentials)(this);
16
+ this.log('Client ID and Issuer URL updated');
17
+ }
18
+ catch (error) {
19
+ this.error(error);
20
+ }
21
+ }
22
+ }
23
+ SetAuthProvider.description = `Sets the Issuer URL and Client ID of the Auth Provider that the CLI will authenticate with.
24
+ Only use this if you are running a self-hosted, air-gapped instance of Opal that uses a custom Auth Provider.
25
+
26
+ Note - you will need an OIDC provider that supports the device_code grant.
27
+ `;
28
+ SetAuthProvider.examples = ['$ opal set-auth-provider --clientID 1234asdf --issuerUrl https://auth.example.com'];
29
+ SetAuthProvider.flags = {
30
+ help: flags_1.SHARED_FLAGS.help,
31
+ clientID: core_1.Flags.string({
32
+ multiple: false,
33
+ description: "Client ID of your Auth Provider",
34
+ required: true
35
+ }),
36
+ issuerUrl: core_1.Flags.string({
37
+ multiple: false,
38
+ description: "Issuer URL of your Auth Provider",
39
+ required: true
40
+ }),
41
+ };
42
+ exports.default = SetAuthProvider;
@@ -24,7 +24,7 @@ class SetToken extends core_1.Command {
24
24
  let email;
25
25
  let organizationID;
26
26
  const existingCreds = await (0, credentials_1.getOpalCredentials)(this, false);
27
- await (0, credentials_1.setOpalCredentials)(this, existingCreds === null || existingCreds === void 0 ? void 0 : existingCreds.email, (existingCreds === null || existingCreds === void 0 ? void 0 : existingCreds.organizationID) || 'unset-org-id', existingCreds === null || existingCreds === void 0 ? void 0 : existingCreds.clientIDCandidate, apiToken || '');
27
+ await (0, credentials_1.setOpalCredentials)(this, existingCreds === null || existingCreds === void 0 ? void 0 : existingCreds.email, (existingCreds === null || existingCreds === void 0 ? void 0 : existingCreds.organizationID) || 'unset-org-id', existingCreds === null || existingCreds === void 0 ? void 0 : existingCreds.clientIDCandidate, apiToken || '', credentials_1.SecretType.ApiToken);
28
28
  // "Representative" authenticated call to check the log-in worked as expected.
29
29
  const { resp: authCheckResp, error: authCheckErr } = await (0, handler_1.runQuery)({
30
30
  command: this,
@@ -1,6 +1,7 @@
1
1
  import { ApolloClient, NormalizedCacheObject } from '@apollo/client/core';
2
2
  import { Command } from '@oclif/core';
3
3
  export declare let client: ApolloClient<NormalizedCacheObject> | null;
4
+ export declare let cookieStr: string;
4
5
  export declare const printResponse: (command: Command, resp: any) => void;
5
6
  export declare const handleError: (command: Command, err: any, resp?: any) => void;
6
7
  export declare const initClient: (command: Command, fetchAccessToken?: boolean) => Promise<void>;
package/lib/lib/apollo.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.initClient = exports.handleError = exports.printResponse = exports.client = void 0;
3
+ exports.initClient = exports.handleError = exports.printResponse = exports.cookieStr = exports.client = void 0;
4
4
  const core_1 = require("@apollo/client/core");
5
5
  const context_1 = require("@apollo/client/link/context");
6
6
  const error_1 = require("@apollo/client/link/error");
@@ -16,7 +16,9 @@ const fetch = require('node-fetch');
16
16
  const https = require('https');
17
17
  const http = require('http');
18
18
  exports.client = null;
19
- let alreadyNotifiedRecommendedVersion = false;
19
+ // This is used to keep track of the auth-session cookie during login, before we can persist it into the credential store
20
+ exports.cookieStr = '';
21
+ let alreadyWarnedAboutVersion = false;
20
22
  const printResponse = (command, resp) => {
21
23
  const filteredJson = JSON.parse(JSON.stringify(resp.data, (k, v) => {
22
24
  if (k === '__typename' || v === null || (Array.isArray(v) && v.length === 0)) {
@@ -57,7 +59,6 @@ const initClient = async (command, fetchAccessToken = true) => {
57
59
  const currentCLIVersion = command.config.version;
58
60
  const configData = (0, config_1.getOrCreateConfigData)(configDir);
59
61
  const opalCreds = await (0, credentials_1.getOpalCredentials)(command, fetchAccessToken);
60
- const accessToken = opalCreds === null || opalCreds === void 0 ? void 0 : opalCreds.accessToken;
61
62
  const organizationID = opalCreds === null || opalCreds === void 0 ? void 0 : opalCreds.organizationID;
62
63
  const httpsAgent = new https.Agent({
63
64
  rejectUnauthorized: !configData[config_1.allowSelfSignedCertsKey],
@@ -74,13 +75,21 @@ const initClient = async (command, fetchAccessToken = true) => {
74
75
  const agent = specifiedUrl.includes('https') ? httpsAgent : httpAgent;
75
76
  const httpLink = (0, core_1.createHttpLink)({
76
77
  uri: `${specifiedUrl}/query`,
78
+ credentials: 'include',
77
79
  fetch,
78
80
  fetchOptions: {
79
81
  agent: agent,
82
+ credentials: 'include'
80
83
  },
81
84
  });
82
85
  const authLink = (0, context_1.setContext)((_, { headers }) => {
83
- const baseHeaders = Object.assign(Object.assign({}, headers), { authorization: `Bearer ${accessToken}`, 'X-Opal-Organization-ID': organizationID, 'X-Opal-Cli-Version': currentCLIVersion });
86
+ const baseHeaders = Object.assign(Object.assign({}, headers), { 'User-Agent': `Opal CLI v${currentCLIVersion}`, 'X-Opal-Organization-ID': organizationID, 'X-Opal-Cli-Version': currentCLIVersion });
87
+ if (opalCreds.secretType === credentials_1.SecretType.ApiToken) {
88
+ baseHeaders.authorization = `Bearer ${opalCreds.secret}`;
89
+ }
90
+ else if (!!opalCreds.secret || !!exports.cookieStr) {
91
+ baseHeaders.cookie = opalCreds.secret || exports.cookieStr;
92
+ }
84
93
  if (customHeaderKey) {
85
94
  return {
86
95
  headers: Object.assign(Object.assign({}, baseHeaders), { [customHeaderKey]: customHeaderValue }),
@@ -90,33 +99,46 @@ const initClient = async (command, fetchAccessToken = true) => {
90
99
  headers: Object.assign({}, baseHeaders),
91
100
  };
92
101
  });
102
+ /**
103
+ * How this check actually works:
104
+ * - no hard fails throughout, just warns
105
+ * - Looks for an 'expected CLI major version' header from the backend, and:
106
+ * - if CLI version is < that, warns you need to upgrade CLI
107
+ * - if CLI version is > that, warns you need to upgrade Opal
108
+ * - also, if there's a 'recommended version' header and CLI version is less, just suggests that you upgrade your CLI
109
+ * - note the backend does not currently send this header)
110
+ */
93
111
  const checkCLIVersion = (operation) => {
94
112
  var _a;
95
113
  const headers = (_a = operation.getContext().response) === null || _a === void 0 ? void 0 : _a.headers;
96
- if (headers) {
114
+ if (!alreadyWarnedAboutVersion && headers) {
115
+ // we expect these versions to be a single number - a major version
97
116
  const expectedCLIMajorVersion = headers.get('Opal-CLI-Expected-Major-Version');
98
- if (expectedCLIMajorVersion) {
117
+ if (expectedCLIMajorVersion && expectedCLIMajorVersion.match(/^\d+$/)) {
99
118
  const semverExpectedMinCLIVersion = `${expectedCLIMajorVersion}.0.0`;
100
119
  if ((0, semver_1.major)(currentCLIVersion) < (0, semver_1.major)(semverExpectedMinCLIVersion)) {
101
- command.warn(chalk_1.default.yellow(`
120
+ command.log(chalk_1.default.yellow(`
102
121
  ❗️ Your CLI is outdated and is incompatible with your Opal server.
103
122
  Expected major version ${chalk_1.default.blueBright(semverExpectedMinCLIVersion)}, but version ${chalk_1.default.blueBright(currentCLIVersion)} is installed.
104
123
  Upgrade using the following command: ${chalk_1.default.blueBright('brew upgrade opalsecurity/brew/opal-security')}\n`));
124
+ alreadyWarnedAboutVersion = true;
105
125
  }
106
126
  else if ((0, semver_1.major)(currentCLIVersion) > (0, semver_1.major)(semverExpectedMinCLIVersion)) {
107
- command.warn(chalk_1.default.yellow(`
127
+ command.log(chalk_1.default.yellow(`
108
128
  ❗️ Uh oh! The currently installed Opal server is outdated. Please contact your Opal admins to let them know they need to upgrade their deployment.\n`));
129
+ alreadyWarnedAboutVersion = true;
109
130
  }
110
131
  }
111
- if (!alreadyNotifiedRecommendedVersion) {
112
- const recommendedCLIMinorVersion = headers.get('Opal-CLI-Recommended-Version');
113
- if (recommendedCLIMinorVersion && (0, semver_1.compare)(currentCLIVersion, recommendedCLIMinorVersion) < 0) {
132
+ const recommendedCLIMajorVersion = headers.get('Opal-CLI-Recommended-Version');
133
+ if (recommendedCLIMajorVersion && recommendedCLIMajorVersion.match(/^\d+$/)) {
134
+ const recommendedSemverString = `${recommendedCLIMajorVersion}.0.0`;
135
+ if (recommendedSemverString && (0, semver_1.major)(currentCLIVersion) < (0, semver_1.major)(recommendedSemverString)) {
114
136
  command.log(chalk_1.default.yellow(`
115
137
  ❗️ Opal recommends that you upgrade your CLI.
116
- Recommended version >=${chalk_1.default.blueBright(recommendedCLIMinorVersion)}, but version ${chalk_1.default.blueBright(currentCLIVersion)} is installed.
117
- Upgrade using the following command: ${chalk_1.default.blueBright('brew upgrade opalsecurity/brew/opal-security')}\n`));
138
+ Recommended version >=${chalk_1.default.blueBright(recommendedSemverString)}, but version ${chalk_1.default.blueBright(currentCLIVersion)} is installed.
139
+ Upgrade using the following command: ${chalk_1.default.blueBright('brew upgrade opalsecurity/brew/opal-security')}\n`));
118
140
  }
119
- alreadyNotifiedRecommendedVersion = true;
141
+ alreadyWarnedAboutVersion = true;
120
142
  }
121
143
  }
122
144
  };
@@ -128,15 +150,18 @@ const initClient = async (command, fetchAccessToken = true) => {
128
150
  });
129
151
  const errorLink = (0, error_1.onError)(({ networkError, operation }) => {
130
152
  var _a;
131
- if (operation.operationName === login_1.CLIAuthSessionCheckName) {
132
- // These operations are triggered after the user just called `opal login` or `opal set-token`,
133
- // so we have special handling if they fail and we don't want to trigger an automatic redirect
134
- // to the login screen again.
135
- return;
136
- }
137
- if (operation.operationName === login_1.CLISignInMethodName) {
138
- // We have special handling for this operation due to the fact that the backend version
153
+ // There's a few GQL operations where we don't want to use this error handler:
154
+ const customErrorOperations = [
155
+ // This is triggered just after the user called `opal login` or `opal set-token`, and has
156
+ // special handling if they fail, so we don't trigger an automatic re-login
157
+ login_1.CLIAuthSessionCheckName,
158
+ // This has special error handling to fall back to an older auth method that uses the OAuth access token
159
+ login_1.CLITokenExchangeName,
160
+ // This has special error handling due to the fact that the backend version
139
161
  // can be out of date and not include the new cliClientId field.
162
+ login_1.CLISignInMethodName,
163
+ ];
164
+ if (customErrorOperations.includes(operation.operationName)) {
140
165
  return;
141
166
  }
142
167
  if (networkError && 'statusCode' in networkError) {
@@ -170,11 +195,25 @@ const initClient = async (command, fetchAccessToken = true) => {
170
195
  }
171
196
  }
172
197
  });
198
+ // Parses our auth-session cookie out of the Set-Cookie response header, saving it locally so we can use it for future requests
199
+ const preserveCookiesLink = new core_1.ApolloLink((operation, forward) => {
200
+ return forward(operation).map(response => {
201
+ var _a, _b, _c, _d;
202
+ const cookieHeaderStr = (_c = (_b = (_a = operation.getContext().response) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.get('Set-Cookie')) !== null && _c !== void 0 ? _c : '';
203
+ const authSessionCookie = (_d = cookieHeaderStr.match(/auth-session=[^;]+;/)) === null || _d === void 0 ? void 0 : _d[0];
204
+ if (!!authSessionCookie) {
205
+ exports.cookieStr = authSessionCookie;
206
+ }
207
+ return response;
208
+ });
209
+ });
173
210
  let link = authLink.concat(httpLink);
174
211
  link = checkCLIVersionLink.concat(link);
212
+ link = preserveCookiesLink.concat(link);
175
213
  link = errorLink.concat(link);
176
214
  exports.client = new core_1.ApolloClient({
177
215
  link: link,
216
+ credentials: 'include',
178
217
  cache: new core_1.InMemoryCache(),
179
218
  });
180
219
  };
package/lib/lib/config.js CHANGED
@@ -47,9 +47,8 @@ const isProduction = (configDir) => {
47
47
  const configData = (0, exports.getOrCreateConfigData)(configDir);
48
48
  // Custom URLs are considered production since it includes on-prem
49
49
  return configData[exports.urlKey] !== 'https://dev.opal.dev' &&
50
- configData[exports.urlKey] !== 'http://localhost:3000' &&
51
- configData[exports.urlKey] !== 'http://localhost:4000' &&
52
50
  configData[exports.urlKey] !== 'https://demo.opal.dev' &&
53
- configData[exports.urlKey] !== 'https://staging.opal.dev';
51
+ configData[exports.urlKey] !== 'https://staging.opal.dev' &&
52
+ !configData[exports.urlKey].match(/https?:\/\/localhost/);
54
53
  };
55
54
  exports.isProduction = isProduction;
@@ -3,9 +3,14 @@ interface OpalCredentials {
3
3
  email?: string;
4
4
  organizationID?: string;
5
5
  clientIDCandidate?: string;
6
- accessToken?: string;
6
+ secret?: string;
7
+ secretType?: SecretType;
7
8
  }
8
- export declare const setOpalCredentials: (command: Command, email: string | undefined, organizationID: string, clientIDCandidate: string | undefined | null, accessToken: string) => Promise<void>;
9
- export declare const getOpalCredentials: (command: Command, includeAccessToken?: boolean) => Promise<OpalCredentials>;
9
+ export declare enum SecretType {
10
+ Cookie = "COOKIE",
11
+ ApiToken = "API_TOKEN"
12
+ }
13
+ export declare const setOpalCredentials: (command: Command, email: string | undefined, organizationID: string, clientIDCandidate: string | undefined | null, secret: string, secretType: SecretType) => Promise<void>;
14
+ export declare const getOpalCredentials: (command: Command, includeAuthSecret?: boolean) => Promise<OpalCredentials>;
10
15
  export declare const removeOpalCredentials: (command: Command) => Promise<void>;
11
16
  export {};
@@ -1,41 +1,52 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.removeOpalCredentials = exports.getOpalCredentials = exports.setOpalCredentials = void 0;
3
+ exports.removeOpalCredentials = exports.getOpalCredentials = exports.setOpalCredentials = exports.SecretType = void 0;
4
4
  const config_1 = require("../config");
5
5
  const keychain_1 = require("./keychain");
6
6
  const localEncryption_1 = require("./localEncryption");
7
- const setOpalCredentials = async (command, email, organizationID, clientIDCandidate, accessToken) => {
7
+ var SecretType;
8
+ (function (SecretType) {
9
+ SecretType["Cookie"] = "COOKIE";
10
+ SecretType["ApiToken"] = "API_TOKEN";
11
+ })(SecretType || (exports.SecretType = SecretType = {}));
12
+ const setOpalCredentials = async (command, email, organizationID, clientIDCandidate, secret, secretType) => {
8
13
  email = email || 'email-unset';
9
14
  const configData = (0, config_1.getOrCreateConfigData)(command.config.configDir);
10
15
  configData.creds = {
11
16
  clientIDCandidate,
12
17
  email,
13
18
  organizationID,
19
+ secretType
14
20
  };
15
21
  (0, config_1.writeConfigData)(command.config.configDir, configData);
16
22
  if (process.platform === "darwin") {
17
- await (0, keychain_1.setTokenInKeychain)(email, accessToken);
23
+ await (0, keychain_1.setSecretInKeychain)(email, secret);
18
24
  }
19
25
  else {
20
- await (0, localEncryption_1.setTokenInConfig)(command, configData, accessToken);
26
+ await (0, localEncryption_1.setSecretInConfig)(command, configData, secret);
21
27
  }
22
28
  };
23
29
  exports.setOpalCredentials = setOpalCredentials;
24
- const getOpalCredentials = async (command, includeAccessToken = true) => {
30
+ const getOpalCredentials = async (command, includeAuthSecret = true) => {
25
31
  var _a, _b;
26
32
  let creds = (_b = (_a = (0, config_1.getOrCreateConfigData)(command.config.configDir)) === null || _a === void 0 ? void 0 : _a.creds) !== null && _b !== void 0 ? _b : {};
27
- if (!includeAccessToken) {
33
+ if (!includeAuthSecret) {
28
34
  return creds;
29
35
  }
30
- let accessToken = null;
36
+ let secret = null;
31
37
  if (process.platform === "darwin") {
32
- accessToken = await (0, keychain_1.getTokenFromKeychain)((creds === null || creds === void 0 ? void 0 : creds.email) || 'email-unset');
38
+ secret = await (0, keychain_1.getSecretFromKeychain)((creds === null || creds === void 0 ? void 0 : creds.email) || 'email-unset');
33
39
  }
34
40
  else {
35
- accessToken = await (0, localEncryption_1.getTokenFromConfig)(creds);
41
+ secret = await (0, localEncryption_1.getSecretFromConfig)(creds);
36
42
  }
37
- if (accessToken) {
38
- creds.accessToken = accessToken;
43
+ if (secret) {
44
+ creds.secret = secret;
45
+ // This is a fallback for users with stored credentials from before we converted to session auth with the CLITokenExchange mutation
46
+ // It will allow them to continue authenticating with an access token in an Authorization header, which will work until we remove support for that
47
+ if (!creds.secretType) {
48
+ creds.secretType = SecretType.ApiToken;
49
+ }
39
50
  }
40
51
  return creds;
41
52
  };
@@ -49,7 +60,7 @@ const removeOpalCredentials = async (command) => {
49
60
  (0, config_1.writeConfigData)(command.config.configDir, configData);
50
61
  // but on OSX, we need an extra step to delete the token from the keychain
51
62
  if (process.platform === "darwin") {
52
- await (0, keychain_1.deleteTokenFromKeychain)(email);
63
+ await (0, keychain_1.deleteSecretFromKeychain)(email);
53
64
  }
54
65
  };
55
66
  exports.removeOpalCredentials = removeOpalCredentials;
@@ -1,3 +1,3 @@
1
- export declare const setTokenInKeychain: (email: string, accessToken: string) => Promise<void>;
2
- export declare const getTokenFromKeychain: (email: string) => Promise<string>;
3
- export declare const deleteTokenFromKeychain: (email: string) => Promise<void>;
1
+ export declare const setSecretInKeychain: (email: string, secret: string) => Promise<void>;
2
+ export declare const getSecretFromKeychain: (email: string) => Promise<string>;
3
+ export declare const deleteSecretFromKeychain: (email: string) => Promise<void>;
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deleteTokenFromKeychain = exports.getTokenFromKeychain = exports.setTokenInKeychain = void 0;
3
+ exports.deleteSecretFromKeychain = exports.getSecretFromKeychain = exports.setSecretInKeychain = void 0;
4
4
  const keychain = require("keychain");
5
5
  const OPAL_KEYCHAIN_SERVICE = 'opal-cli';
6
6
  // On OSX, we can easily work with the system Keychain to store the access token.
7
- const setTokenInKeychain = async (email, accessToken) => {
7
+ const setSecretInKeychain = async (email, secret) => {
8
8
  return new Promise((resolve, reject) => {
9
- keychain.setPassword({ account: email, service: OPAL_KEYCHAIN_SERVICE, password: accessToken }, function (err) {
9
+ keychain.setPassword({ account: email, service: OPAL_KEYCHAIN_SERVICE, password: secret }, function (err) {
10
10
  if (err) {
11
11
  reject(err);
12
12
  }
@@ -16,8 +16,8 @@ const setTokenInKeychain = async (email, accessToken) => {
16
16
  });
17
17
  });
18
18
  };
19
- exports.setTokenInKeychain = setTokenInKeychain;
20
- const getTokenFromKeychain = async (email) => {
19
+ exports.setSecretInKeychain = setSecretInKeychain;
20
+ const getSecretFromKeychain = async (email) => {
21
21
  return new Promise((resolve, reject) => {
22
22
  keychain.getPassword({ account: email, service: OPAL_KEYCHAIN_SERVICE }, function (err, password) {
23
23
  if (err && (err === null || err === void 0 ? void 0 : err.type) !== 'PasswordNotFoundError') {
@@ -29,8 +29,8 @@ const getTokenFromKeychain = async (email) => {
29
29
  });
30
30
  });
31
31
  };
32
- exports.getTokenFromKeychain = getTokenFromKeychain;
33
- const deleteTokenFromKeychain = async (email) => {
32
+ exports.getSecretFromKeychain = getSecretFromKeychain;
33
+ const deleteSecretFromKeychain = async (email) => {
34
34
  return new Promise((resolve) => {
35
35
  keychain.deletePassword({ service: OPAL_KEYCHAIN_SERVICE, account: email }, function () {
36
36
  // we might get errors here if the password doesn't exist, which we want to swallow
@@ -38,4 +38,4 @@ const deleteTokenFromKeychain = async (email) => {
38
38
  });
39
39
  });
40
40
  };
41
- exports.deleteTokenFromKeychain = deleteTokenFromKeychain;
41
+ exports.deleteSecretFromKeychain = deleteSecretFromKeychain;
@@ -1,3 +1,3 @@
1
1
  import { Command } from '@oclif/core';
2
- export declare const setTokenInConfig: (command: Command, configData: Record<string, any>, accessToken: string) => Promise<void>;
3
- export declare const getTokenFromConfig: (credsConfig: Record<string, any>) => Promise<string | undefined>;
2
+ export declare const setSecretInConfig: (command: Command, configData: Record<string, any>, secret: string) => Promise<void>;
3
+ export declare const getSecretFromConfig: (credsConfig: Record<string, any>) => Promise<string | undefined>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getTokenFromConfig = exports.setTokenInConfig = void 0;
3
+ exports.getSecretFromConfig = exports.setSecretInConfig = void 0;
4
4
  const inquirer = require("inquirer");
5
5
  const argon2 = require("argon2");
6
6
  const crypto_1 = require("crypto");
@@ -28,7 +28,7 @@ const promptForPassword = async (msg) => {
28
28
  ]);
29
29
  return customKey;
30
30
  };
31
- const setTokenInConfig = async (command, configData, accessToken) => {
31
+ const setSecretInConfig = async (command, configData, secret) => {
32
32
  if (!password) {
33
33
  password = await promptForPassword('Enter a password to encrypt your credentials: ');
34
34
  }
@@ -36,20 +36,20 @@ const setTokenInConfig = async (command, configData, accessToken) => {
36
36
  const key = await argon2.hash(password, { hashLength: 32, raw: true, salt: salt });
37
37
  const iv = (0, crypto_1.randomBytes)(12);
38
38
  const cipher = (0, crypto_1.createCipheriv)(ENCRYPTION_ALGORITHM, key, iv);
39
- let accessTokenEncrypted = cipher.update(accessToken, 'utf8', 'base64');
40
- accessTokenEncrypted += cipher.final('base64');
39
+ let secretEncrypted = cipher.update(secret, 'utf8', 'base64');
40
+ secretEncrypted += cipher.final('base64');
41
41
  // In addition to storing the encrypted text, we need to store the argon2 salt, and AES authTag + IV so we can decrypt
42
42
  configData.creds.accessTokenDetails = {
43
- encryptedText: accessTokenEncrypted,
43
+ encryptedText: secretEncrypted,
44
44
  authTag: cipher.getAuthTag().toString('base64'),
45
45
  salt: salt.toString('base64'),
46
46
  iv: iv.toString('base64')
47
47
  };
48
48
  (0, config_1.writeConfigData)(command.config.configDir, configData);
49
49
  };
50
- exports.setTokenInConfig = setTokenInConfig;
51
- const getTokenFromConfig = async (credsConfig) => {
52
- let accessToken;
50
+ exports.setSecretInConfig = setSecretInConfig;
51
+ const getSecretFromConfig = async (credsConfig) => {
52
+ let secret;
53
53
  if (credsConfig === null || credsConfig === void 0 ? void 0 : credsConfig.accessTokenDetails) {
54
54
  if (!password) {
55
55
  password = await promptForPassword('Enter the password used to encrypt your credentials: ');
@@ -60,8 +60,8 @@ const getTokenFromConfig = async (credsConfig) => {
60
60
  try {
61
61
  const decipher = (0, crypto_1.createDecipheriv)(ENCRYPTION_ALGORITHM, key, iv);
62
62
  decipher.setAuthTag(Buffer.from(credsConfig.accessTokenDetails.authTag, 'base64'));
63
- accessToken = decipher.update(credsConfig.accessTokenDetails.encryptedText, 'base64', 'utf8');
64
- accessToken += decipher.final('utf8');
63
+ secret = decipher.update(credsConfig.accessTokenDetails.encryptedText, 'base64', 'utf8');
64
+ secret += decipher.final('utf8');
65
65
  }
66
66
  catch (error) {
67
67
  console.error("ERROR: Failed to decrypt local credentials.\n" +
@@ -70,6 +70,6 @@ const getTokenFromConfig = async (credsConfig) => {
70
70
  }
71
71
  delete credsConfig.accessTokenDetails;
72
72
  }
73
- return accessToken;
73
+ return secret;
74
74
  };
75
- exports.getTokenFromConfig = getTokenFromConfig;
75
+ exports.getSecretFromConfig = getSecretFromConfig;
@@ -1,5 +1,36 @@
1
1
  {
2
2
  "commands": {
3
+ "clear-auth-provider": {
4
+ "aliases": [],
5
+ "args": {},
6
+ "description": "Clears the custom Issuer URL and Client ID set by set-airgap-auth, returning to the default.",
7
+ "examples": [
8
+ "$ opal clear-auth-provider"
9
+ ],
10
+ "flags": {
11
+ "help": {
12
+ "char": "h",
13
+ "description": "Show CLI help.",
14
+ "name": "help",
15
+ "allowNo": false,
16
+ "type": "boolean"
17
+ }
18
+ },
19
+ "hasDynamicHelp": false,
20
+ "hiddenAliases": [],
21
+ "id": "clear-auth-provider",
22
+ "pluginAlias": "opal-security",
23
+ "pluginName": "opal-security",
24
+ "pluginType": "core",
25
+ "strict": true,
26
+ "enableJsonFlag": false,
27
+ "isESM": false,
28
+ "relativePath": [
29
+ "lib",
30
+ "commands",
31
+ "clear-auth-provider.js"
32
+ ]
33
+ },
3
34
  "curl-example": {
4
35
  "aliases": [],
5
36
  "args": {},
@@ -97,10 +128,13 @@
97
128
  "logout.js"
98
129
  ]
99
130
  },
100
- "migrate-creds": {
131
+ "set-auth-provider": {
101
132
  "aliases": [],
102
133
  "args": {},
103
- "description": "Migrates credentials from old keystore to new store. Should only need to be run once",
134
+ "description": "Sets the Issuer URL and Client ID of the Auth Provider that the CLI will authenticate with.\n Only use this if you are running a self-hosted, air-gapped instance of Opal that uses a custom Auth Provider.\n\n Note - you will need an OIDC provider that supports the device_code grant.\n ",
135
+ "examples": [
136
+ "$ opal set-auth-provider --clientID 1234asdf --issuerUrl https://auth.example.com"
137
+ ],
104
138
  "flags": {
105
139
  "help": {
106
140
  "char": "h",
@@ -108,11 +142,27 @@
108
142
  "name": "help",
109
143
  "allowNo": false,
110
144
  "type": "boolean"
145
+ },
146
+ "clientID": {
147
+ "description": "Client ID of your Auth Provider",
148
+ "name": "clientID",
149
+ "required": true,
150
+ "hasDynamicHelp": false,
151
+ "multiple": false,
152
+ "type": "option"
153
+ },
154
+ "issuerUrl": {
155
+ "description": "Issuer URL of your Auth Provider",
156
+ "name": "issuerUrl",
157
+ "required": true,
158
+ "hasDynamicHelp": false,
159
+ "multiple": false,
160
+ "type": "option"
111
161
  }
112
162
  },
113
163
  "hasDynamicHelp": false,
114
164
  "hiddenAliases": [],
115
- "id": "migrate-creds",
165
+ "id": "set-auth-provider",
116
166
  "pluginAlias": "opal-security",
117
167
  "pluginName": "opal-security",
118
168
  "pluginType": "core",
@@ -122,7 +172,7 @@
122
172
  "relativePath": [
123
173
  "lib",
124
174
  "commands",
125
- "migrate-creds.js"
175
+ "set-auth-provider.js"
126
176
  ]
127
177
  },
128
178
  "set-custom-header": {
@@ -752,5 +802,5 @@
752
802
  ]
753
803
  }
754
804
  },
755
- "version": "2.3.4"
805
+ "version": "3.0.0"
756
806
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opal-security",
3
3
  "description": "Opal allows you to centrally manage access to all of your sensitive systems.",
4
- "version": "2.3.4",
4
+ "version": "3.0.0",
5
5
  "author": "Stephen Cobbe",
6
6
  "bin": {
7
7
  "opal": "./bin/run"
@@ -1,48 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const core_1 = require("@oclif/core");
4
- const keytar = require("keytar");
5
- const credentials_1 = require("../lib/credentials");
6
- const flags_1 = require("../lib/flags");
7
- const OPAL_KEYTAR_CREDS_KEY = 'opal';
8
- /**
9
- * This command helps users migrate from the old credential store w/ keytar to the new credential store
10
- * It should only be recommended to users on OSX, since keytar does not reliably work on linux/WSL
11
- *
12
- * TODO: delete this after some time has passed, and users have likely migrated their credentials over
13
- */
14
- const removeKeytarCreds = async () => {
15
- const keyContents = await keytar.findCredentials(OPAL_KEYTAR_CREDS_KEY);
16
- keyContents === null || keyContents === void 0 ? void 0 : keyContents.forEach(credential => keytar.deletePassword(OPAL_KEYTAR_CREDS_KEY, credential.account));
17
- };
18
- const getKeytarCreds = async () => {
19
- const keyContents = await keytar.findCredentials(OPAL_KEYTAR_CREDS_KEY);
20
- if (!keyContents[0]) {
21
- return undefined;
22
- }
23
- const { account, password } = keyContents[0];
24
- const parts = account.split('|') || [];
25
- return {
26
- email: parts[0],
27
- organizationID: parts[1],
28
- clientIDCandidate: parts[2],
29
- accessToken: password
30
- };
31
- };
32
- class MigrateCreds extends core_1.Command {
33
- async run() {
34
- const creds = await getKeytarCreds();
35
- if (!creds) {
36
- this.log("No credentials found in system keystore that need to be migrated");
37
- return;
38
- }
39
- (0, credentials_1.setOpalCredentials)(this, creds.email, creds.organizationID, creds.clientIDCandidate, creds.accessToken);
40
- await removeKeytarCreds();
41
- this.log("Successfully migrated credentials from system keystore to new store. You should now be able to use the CLI normally, without re-authenticating");
42
- }
43
- }
44
- MigrateCreds.description = 'Migrates credentials from old keystore to new store. Should only need to be run once';
45
- MigrateCreds.flags = {
46
- help: flags_1.SHARED_FLAGS.help,
47
- };
48
- exports.default = MigrateCreds;