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 +65 -35
- package/lib/commands/{migrate-creds.d.ts → clear-auth-provider.d.ts} +2 -1
- package/lib/commands/clear-auth-provider.js +28 -0
- package/lib/commands/curl-example.js +9 -2
- package/lib/commands/login.d.ts +1 -0
- package/lib/commands/login.js +73 -21
- package/lib/commands/set-auth-provider.d.ts +11 -0
- package/lib/commands/set-auth-provider.js +42 -0
- package/lib/commands/set-token.js +1 -1
- package/lib/lib/apollo.d.ts +1 -0
- package/lib/lib/apollo.js +61 -22
- package/lib/lib/config.js +2 -3
- package/lib/lib/credentials/index.d.ts +8 -3
- package/lib/lib/credentials/index.js +23 -12
- package/lib/lib/credentials/keychain.d.ts +3 -3
- package/lib/lib/credentials/keychain.js +8 -8
- package/lib/lib/credentials/localEncryption.d.ts +2 -2
- package/lib/lib/credentials/localEncryption.js +12 -12
- package/oclif.manifest.json +55 -5
- package/package.json +1 -1
- package/lib/commands/migrate-creds.js +0 -48
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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 "
|
|
25
|
+
--header "${authStr}" \\
|
|
19
26
|
--header "X-Opal-Organization-ID: ${organizationID}"
|
|
20
27
|
`);
|
|
21
28
|
}
|
package/lib/commands/login.d.ts
CHANGED
|
@@ -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[];
|
package/lib/commands/login.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
150
|
-
if (
|
|
151
|
-
|
|
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
|
-
|
|
195
|
+
clientID = CLIENT_ID_PROD;
|
|
155
196
|
}
|
|
156
197
|
else {
|
|
157
|
-
|
|
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:
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 || !((
|
|
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
|
-
|
|
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,
|
package/lib/lib/apollo.d.ts
CHANGED
|
@@ -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
|
-
|
|
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), {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
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
|
-
|
|
6
|
+
secret?: string;
|
|
7
|
+
secretType?: SecretType;
|
|
7
8
|
}
|
|
8
|
-
export declare
|
|
9
|
-
|
|
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
|
-
|
|
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.
|
|
23
|
+
await (0, keychain_1.setSecretInKeychain)(email, secret);
|
|
18
24
|
}
|
|
19
25
|
else {
|
|
20
|
-
await (0, localEncryption_1.
|
|
26
|
+
await (0, localEncryption_1.setSecretInConfig)(command, configData, secret);
|
|
21
27
|
}
|
|
22
28
|
};
|
|
23
29
|
exports.setOpalCredentials = setOpalCredentials;
|
|
24
|
-
const getOpalCredentials = async (command,
|
|
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 (!
|
|
33
|
+
if (!includeAuthSecret) {
|
|
28
34
|
return creds;
|
|
29
35
|
}
|
|
30
|
-
let
|
|
36
|
+
let secret = null;
|
|
31
37
|
if (process.platform === "darwin") {
|
|
32
|
-
|
|
38
|
+
secret = await (0, keychain_1.getSecretFromKeychain)((creds === null || creds === void 0 ? void 0 : creds.email) || 'email-unset');
|
|
33
39
|
}
|
|
34
40
|
else {
|
|
35
|
-
|
|
41
|
+
secret = await (0, localEncryption_1.getSecretFromConfig)(creds);
|
|
36
42
|
}
|
|
37
|
-
if (
|
|
38
|
-
creds.
|
|
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.
|
|
63
|
+
await (0, keychain_1.deleteSecretFromKeychain)(email);
|
|
53
64
|
}
|
|
54
65
|
};
|
|
55
66
|
exports.removeOpalCredentials = removeOpalCredentials;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
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.
|
|
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
|
|
7
|
+
const setSecretInKeychain = async (email, secret) => {
|
|
8
8
|
return new Promise((resolve, reject) => {
|
|
9
|
-
keychain.setPassword({ account: email, service: OPAL_KEYCHAIN_SERVICE, password:
|
|
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.
|
|
20
|
-
const
|
|
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.
|
|
33
|
-
const
|
|
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.
|
|
41
|
+
exports.deleteSecretFromKeychain = deleteSecretFromKeychain;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
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.
|
|
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
|
|
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
|
|
40
|
-
|
|
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:
|
|
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.
|
|
51
|
-
const
|
|
52
|
-
let
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
73
|
+
return secret;
|
|
74
74
|
};
|
|
75
|
-
exports.
|
|
75
|
+
exports.getSecretFromConfig = getSecretFromConfig;
|
package/oclif.manifest.json
CHANGED
|
@@ -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
|
-
"
|
|
131
|
+
"set-auth-provider": {
|
|
101
132
|
"aliases": [],
|
|
102
133
|
"args": {},
|
|
103
|
-
"description": "
|
|
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": "
|
|
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
|
-
"
|
|
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": "
|
|
805
|
+
"version": "3.0.0"
|
|
756
806
|
}
|
package/package.json
CHANGED
|
@@ -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;
|