@p0security/cli 0.8.1 → 0.8.3
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/dist/commands/__tests__/grant.test.d.ts +1 -0
- package/dist/commands/__tests__/grant.test.js +55 -0
- package/dist/commands/grant.d.ts +4 -0
- package/dist/commands/grant.js +17 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/request.d.ts +0 -8
- package/dist/commands/request.js +3 -102
- package/dist/commands/scp.d.ts +1 -1
- package/dist/commands/scp.js +5 -4
- package/dist/commands/shared/index.d.ts +4 -0
- package/dist/commands/shared/index.js +67 -0
- package/dist/commands/shared/request.d.ts +14 -0
- package/dist/commands/shared/request.js +115 -0
- package/dist/commands/{shared.d.ts → shared/ssh.d.ts} +12 -22
- package/dist/commands/{shared.js → shared/ssh.js} +21 -75
- package/dist/commands/ssh.d.ts +1 -1
- package/dist/commands/ssh.js +3 -3
- package/dist/common/retry.d.ts +9 -0
- package/dist/common/retry.js +50 -0
- package/dist/drivers/auth.d.ts +1 -1
- package/dist/drivers/auth.js +7 -3
- package/dist/plugins/aws/config.d.ts +1 -1
- package/dist/plugins/aws/idc/index.d.ts +16 -0
- package/dist/plugins/aws/idc/index.js +150 -0
- package/dist/plugins/aws/ssh.d.ts +3 -3
- package/dist/plugins/aws/ssh.js +75 -11
- package/dist/plugins/aws/types.d.ts +29 -5
- package/dist/plugins/google/ssh.d.ts +5 -13
- package/dist/plugins/google/ssh.js +71 -10
- package/dist/plugins/google/types.d.ts +11 -3
- package/dist/plugins/login.d.ts +3 -0
- package/dist/plugins/login.js +10 -0
- package/dist/plugins/oidc/login.d.ts +33 -2
- package/dist/plugins/oidc/login.js +100 -60
- package/dist/plugins/okta/aws.d.ts +1 -1
- package/dist/plugins/okta/aws.js +2 -2
- package/dist/plugins/okta/login.js +11 -1
- package/dist/plugins/ping/login.d.ts +2 -1
- package/dist/plugins/ping/login.js +11 -1
- package/dist/plugins/ssh/index.d.ts +3 -2
- package/dist/plugins/ssh/index.js +65 -84
- package/dist/types/aws/oidc.d.ts +36 -0
- package/dist/types/aws/oidc.js +12 -0
- package/dist/types/oidc.d.ts +21 -0
- package/dist/types/request.d.ts +2 -7
- package/dist/types/ssh.d.ts +47 -0
- package/dist/types/ssh.js +5 -0
- package/package.json +4 -3
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.requestToSsh = exports.provisionRequest = exports.
|
|
12
|
+
exports.requestToSsh = exports.provisionRequest = exports.isSudoCommand = exports.SSH_PROVIDERS = void 0;
|
|
13
13
|
/** Copyright © 2024-present P0 Security
|
|
14
14
|
|
|
15
15
|
This file is part of @p0security/cli
|
|
@@ -20,93 +20,49 @@ This file is part of @p0security/cli
|
|
|
20
20
|
|
|
21
21
|
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
22
22
|
**/
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const request_2 = require("./request");
|
|
23
|
+
const _1 = require(".");
|
|
24
|
+
const keys_1 = require("../../common/keys");
|
|
25
|
+
const firestore_1 = require("../../drivers/firestore");
|
|
26
|
+
const stdio_1 = require("../../drivers/stdio");
|
|
27
|
+
const ssh_1 = require("../../plugins/aws/ssh");
|
|
28
|
+
const ssh_2 = require("../../plugins/google/ssh");
|
|
29
|
+
const ssh_3 = require("../../types/ssh");
|
|
30
|
+
const request_1 = require("./request");
|
|
32
31
|
const firestore_2 = require("firebase/firestore");
|
|
33
32
|
const lodash_1 = require("lodash");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// The prefix of installed SSH accounts in P0 is the provider name
|
|
39
|
-
exports.SUPPORTED_PROVIDERS = ["aws", "gcloud"];
|
|
33
|
+
exports.SSH_PROVIDERS = {
|
|
34
|
+
aws: ssh_1.awsSshProvider,
|
|
35
|
+
gcloud: ssh_2.gcpSshProvider,
|
|
36
|
+
};
|
|
40
37
|
const validateSshInstall = (authn, args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
38
|
var _a;
|
|
42
39
|
const configDoc = yield (0, firestore_2.getDoc)((0, firestore_1.doc)(`o/${authn.identity.org.tenantId}/integrations/ssh`));
|
|
43
40
|
const configItems = (_a = configDoc.data()) === null || _a === void 0 ? void 0 : _a["iam-write"];
|
|
44
41
|
const providersToCheck = args.provider
|
|
45
42
|
? [args.provider]
|
|
46
|
-
:
|
|
43
|
+
: ssh_3.SupportedSshProviders;
|
|
47
44
|
const items = Object.entries(configItems !== null && configItems !== void 0 ? configItems : {}).filter(([key, value]) => value.state == "installed" &&
|
|
48
45
|
providersToCheck.some((prefix) => key.startsWith(prefix)));
|
|
49
46
|
if (items.length === 0) {
|
|
50
47
|
throw "This organization is not configured for SSH access via the P0 CLI";
|
|
51
48
|
}
|
|
52
49
|
});
|
|
53
|
-
/** Waits until P0 grants access for a request */
|
|
54
|
-
const waitForProvisioning = (authn, requestId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
-
let cancel = undefined;
|
|
56
|
-
const result = yield new Promise((resolve, reject) => {
|
|
57
|
-
let isResolved = false;
|
|
58
|
-
const unsubscribe = (0, firestore_2.onSnapshot)((0, firestore_1.doc)(`o/${authn.identity.org.tenantId}/permission-requests/${requestId}`), (snap) => {
|
|
59
|
-
const data = snap.data();
|
|
60
|
-
if (!data)
|
|
61
|
-
return;
|
|
62
|
-
if (request_1.DONE_STATUSES.includes(data.status)) {
|
|
63
|
-
resolve(data);
|
|
64
|
-
}
|
|
65
|
-
else if (request_1.DENIED_STATUSES.includes(data.status)) {
|
|
66
|
-
reject("Your access request was denied");
|
|
67
|
-
}
|
|
68
|
-
else if (request_1.ERROR_STATUSES.includes(data.status)) {
|
|
69
|
-
reject("Your access request encountered an error (see Slack for details)");
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
isResolved = true;
|
|
75
|
-
unsubscribe();
|
|
76
|
-
});
|
|
77
|
-
// Skip timeout in test; it holds a ref longer than the test lasts
|
|
78
|
-
if (process.env.NODE_ENV === "test")
|
|
79
|
-
return;
|
|
80
|
-
cancel = setTimeout(() => {
|
|
81
|
-
if (!isResolved) {
|
|
82
|
-
unsubscribe();
|
|
83
|
-
reject("Timeout awaiting SSH access grant");
|
|
84
|
-
}
|
|
85
|
-
}, GRANT_TIMEOUT_MILLIS);
|
|
86
|
-
});
|
|
87
|
-
clearTimeout(cancel);
|
|
88
|
-
return result;
|
|
89
|
-
});
|
|
90
50
|
const pluginToCliRequest = (request, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
-
return request.permission.spec.type
|
|
92
|
-
? Object.assign(Object.assign({}, request), { cliLocalData: {
|
|
93
|
-
linuxUserName: yield (0, ssh_key_1.importSshKey)(request.permission.spec.publicKey, options),
|
|
94
|
-
} })
|
|
95
|
-
: request.permission.spec.type === "aws"
|
|
96
|
-
? request
|
|
97
|
-
: (0, util_1.throwAssertNever)(request.permission.spec);
|
|
51
|
+
return yield exports.SSH_PROVIDERS[request.permission.spec.type].toCliRequest(request, options);
|
|
98
52
|
});
|
|
53
|
+
const isSudoCommand = (args) => args.sudo || args.command === "sudo";
|
|
54
|
+
exports.isSudoCommand = isSudoCommand;
|
|
99
55
|
const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
56
|
yield validateSshInstall(authn, args);
|
|
101
57
|
const { publicKey, privateKey } = yield (0, keys_1.createKeyPair)();
|
|
102
|
-
const response = yield (0,
|
|
58
|
+
const response = yield (0, request_1.request)("request")(Object.assign(Object.assign({}, (0, lodash_1.pick)(args, "$0", "_")), { arguments: [
|
|
103
59
|
"ssh",
|
|
104
60
|
"session",
|
|
105
61
|
destination,
|
|
106
62
|
"--public-key",
|
|
107
63
|
publicKey,
|
|
108
64
|
...(args.provider ? ["--provider", args.provider] : []),
|
|
109
|
-
...(
|
|
65
|
+
...((0, exports.isSudoCommand)(args) ? ["--sudo"] : []),
|
|
110
66
|
...(args.reason ? ["--reason", args.reason] : []),
|
|
111
67
|
...(args.account ? ["--account", args.account] : []),
|
|
112
68
|
], wait: true }), authn, { message: "approval-required" });
|
|
@@ -117,7 +73,7 @@ const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0,
|
|
|
117
73
|
const { id, isPreexisting } = response;
|
|
118
74
|
if (!isPreexisting)
|
|
119
75
|
(0, stdio_1.print2)("Waiting for access to be provisioned");
|
|
120
|
-
const provisionedRequest = yield waitForProvisioning(authn, id);
|
|
76
|
+
const provisionedRequest = yield (0, _1.waitForProvisioning)(authn, id);
|
|
121
77
|
if (provisionedRequest.permission.spec.publicKey !== publicKey) {
|
|
122
78
|
throw "Public key mismatch. Please revoke the request and try again.";
|
|
123
79
|
}
|
|
@@ -127,15 +83,5 @@ const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0,
|
|
|
127
83
|
return { request: cliRequest, publicKey, privateKey };
|
|
128
84
|
});
|
|
129
85
|
exports.provisionRequest = provisionRequest;
|
|
130
|
-
const requestToSsh = (request) =>
|
|
131
|
-
if (request.permission.spec.type === "aws") {
|
|
132
|
-
return (0, ssh_1.awsRequestToSsh)(request);
|
|
133
|
-
}
|
|
134
|
-
else if (request.permission.spec.type === "gcloud") {
|
|
135
|
-
return (0, ssh_2.gcpRequestToSsh)(request);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
throw (0, util_1.assertNever)(request.permission.spec);
|
|
139
|
-
}
|
|
140
|
-
};
|
|
86
|
+
const requestToSsh = (request) => exports.SSH_PROVIDERS[request.permission.spec.type].requestToSsh(request);
|
|
141
87
|
exports.requestToSsh = requestToSsh;
|
package/dist/commands/ssh.d.ts
CHANGED
package/dist/commands/ssh.js
CHANGED
|
@@ -23,7 +23,7 @@ You should have received a copy of the GNU General Public License along with @p0
|
|
|
23
23
|
const auth_1 = require("../drivers/auth");
|
|
24
24
|
const firestore_1 = require("../drivers/firestore");
|
|
25
25
|
const ssh_1 = require("../plugins/ssh");
|
|
26
|
-
const
|
|
26
|
+
const ssh_2 = require("./shared/ssh");
|
|
27
27
|
const sshCommand = (yargs) => yargs.command("ssh <destination> [command [arguments..]]", "SSH into a virtual machine", (yargs) => yargs
|
|
28
28
|
.positional("destination", {
|
|
29
29
|
type: "string",
|
|
@@ -86,10 +86,10 @@ const sshAction = (args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
86
86
|
// Prefix is required because the backend uses it to determine that this is an AWS request
|
|
87
87
|
const authn = yield (0, auth_1.authenticate)();
|
|
88
88
|
const destination = args.destination;
|
|
89
|
-
const result = yield (0,
|
|
89
|
+
const result = yield (0, ssh_2.provisionRequest)(authn, args, destination);
|
|
90
90
|
if (!result) {
|
|
91
91
|
throw "Server did not return a request id. Please contact support@p0.dev for assistance.";
|
|
92
92
|
}
|
|
93
93
|
const { request, privateKey } = result;
|
|
94
|
-
yield (0, ssh_1.sshOrScp)(authn, (0,
|
|
94
|
+
yield (0, ssh_1.sshOrScp)(authn, (0, ssh_2.requestToSsh)(request), Object.assign(Object.assign({}, args), { destination }), privateKey);
|
|
95
95
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retries an operation with a delay between retries
|
|
3
|
+
* @param operation operation to retry
|
|
4
|
+
* @param shouldRetry predicate to evaluate on error; will retry only if this is true
|
|
5
|
+
* @param retries number of retries
|
|
6
|
+
* @param delay time to wait before retrying
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
export declare function retryWithSleep<T>(operation: () => Promise<T>, shouldRetry: (error: unknown) => boolean, retries?: number, delayMs?: number): Promise<T>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.retryWithSleep = void 0;
|
|
13
|
+
/** Copyright © 2024-present P0 Security
|
|
14
|
+
|
|
15
|
+
This file is part of @p0security/cli
|
|
16
|
+
|
|
17
|
+
@p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
|
|
18
|
+
|
|
19
|
+
@p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
20
|
+
|
|
21
|
+
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
22
|
+
**/
|
|
23
|
+
const util_1 = require("../util");
|
|
24
|
+
const MAX_RETRIES = 3;
|
|
25
|
+
const MAX_RETRY_BACK_OFF_TIME = 10000;
|
|
26
|
+
/**
|
|
27
|
+
* Retries an operation with a delay between retries
|
|
28
|
+
* @param operation operation to retry
|
|
29
|
+
* @param shouldRetry predicate to evaluate on error; will retry only if this is true
|
|
30
|
+
* @param retries number of retries
|
|
31
|
+
* @param delay time to wait before retrying
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
function retryWithSleep(operation, shouldRetry, retries = MAX_RETRIES, delayMs = MAX_RETRY_BACK_OFF_TIME) {
|
|
35
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
try {
|
|
37
|
+
return yield operation();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (shouldRetry(error)) {
|
|
41
|
+
if (retries > 0) {
|
|
42
|
+
yield (0, util_1.sleep)(delayMs);
|
|
43
|
+
return yield retryWithSleep(operation, shouldRetry, retries - 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
exports.retryWithSleep = retryWithSleep;
|
package/dist/drivers/auth.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Authn, Identity } from "../types/identity";
|
|
|
2
2
|
export declare const IDENTITY_FILE_PATH: string;
|
|
3
3
|
export declare const cached: <T>(name: string, loader: () => Promise<T>, options: {
|
|
4
4
|
duration: number;
|
|
5
|
-
}) => Promise<T>;
|
|
5
|
+
}, hasExpired?: ((data: T) => boolean) | undefined) => Promise<T>;
|
|
6
6
|
export declare const loadCredentials: (options?: {
|
|
7
7
|
noRefresh?: boolean;
|
|
8
8
|
}) => Promise<Identity>;
|
package/dist/drivers/auth.js
CHANGED
|
@@ -51,7 +51,7 @@ const auth_1 = require("firebase/auth");
|
|
|
51
51
|
const fs = __importStar(require("fs/promises"));
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
53
|
exports.IDENTITY_FILE_PATH = path.join(util_1.P0_PATH, "identity.json");
|
|
54
|
-
const cached = (name, loader, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
const cached = (name, loader, options, hasExpired) => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
55
|
var _a;
|
|
56
56
|
const cachePath = path.join(path.dirname(exports.IDENTITY_FILE_PATH), "cache");
|
|
57
57
|
// Following lines sanitize input
|
|
@@ -74,8 +74,12 @@ const cached = (name, loader, options) => __awaiter(void 0, void 0, void 0, func
|
|
|
74
74
|
yield fs.rm(loc);
|
|
75
75
|
return yield loadCache();
|
|
76
76
|
}
|
|
77
|
-
const data = yield fs.readFile(loc);
|
|
78
|
-
|
|
77
|
+
const data = JSON.parse((yield fs.readFile(loc)).toString("utf-8"));
|
|
78
|
+
if (hasExpired === null || hasExpired === void 0 ? void 0 : hasExpired(data)) {
|
|
79
|
+
yield fs.rm(loc);
|
|
80
|
+
return yield loadCache();
|
|
81
|
+
}
|
|
82
|
+
return data;
|
|
79
83
|
}
|
|
80
84
|
catch (error) {
|
|
81
85
|
if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT")
|
|
@@ -4,7 +4,7 @@ export declare const getAwsConfig: (authn: Authn, account: string | undefined) =
|
|
|
4
4
|
config: {
|
|
5
5
|
label?: string | undefined;
|
|
6
6
|
state: string;
|
|
7
|
-
login?:
|
|
7
|
+
login?: import("./types").AwsLogin | undefined;
|
|
8
8
|
id: string;
|
|
9
9
|
};
|
|
10
10
|
}>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AWSClientInformation } from "../../../types/aws/oidc";
|
|
2
|
+
import { AwsCredentials } from "../types";
|
|
3
|
+
export declare const registerClient: (region: string) => Promise<AWSClientInformation>;
|
|
4
|
+
/**
|
|
5
|
+
* Returns AWS credentials for the specified account and permission set for the authorized user
|
|
6
|
+
* @param args accountId, permissionSet and idc to assume role associated with the permission set
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
export declare const assumeRoleWithIdc: (args: {
|
|
10
|
+
accountId?: string;
|
|
11
|
+
permissionSet: string;
|
|
12
|
+
idc: {
|
|
13
|
+
id: string;
|
|
14
|
+
region: string;
|
|
15
|
+
};
|
|
16
|
+
}) => Promise<AwsCredentials>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.assumeRoleWithIdc = exports.registerClient = void 0;
|
|
13
|
+
/** Copyright © 2024-present P0 Security
|
|
14
|
+
|
|
15
|
+
This file is part of @p0security/cli
|
|
16
|
+
|
|
17
|
+
@p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
|
|
18
|
+
|
|
19
|
+
@p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
20
|
+
|
|
21
|
+
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
22
|
+
**/
|
|
23
|
+
const fetch_1 = require("../../../common/fetch");
|
|
24
|
+
const retry_1 = require("../../../common/retry");
|
|
25
|
+
const auth_1 = require("../../../drivers/auth");
|
|
26
|
+
const login_1 = require("../../oidc/login");
|
|
27
|
+
const HOURS = 60 * 60 * 1000;
|
|
28
|
+
const DAYS = 24 * HOURS;
|
|
29
|
+
const AWS_TOKEN_EXPIRY = 1 * HOURS;
|
|
30
|
+
const AWS_CLIENT_TOKEN_EXPIRY = 90 * DAYS; // the token has lifetime of 90 days
|
|
31
|
+
const AWS_SSO_SCOPES = ["sso:account:access"];
|
|
32
|
+
const registerClient = (region) => __awaiter(void 0, void 0, void 0, function* () {
|
|
33
|
+
return yield (0, auth_1.cached)("aws-idc-client", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
const init = {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
clientName: "p0Cli",
|
|
38
|
+
clientType: "public",
|
|
39
|
+
grantTypes: [login_1.DEVICE_GRANT_TYPE],
|
|
40
|
+
scopes: AWS_SSO_SCOPES,
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
const response = yield fetch(`https://oidc.${region}.amazonaws.com/client/register`, init);
|
|
44
|
+
return yield response.json();
|
|
45
|
+
}), { duration: AWS_CLIENT_TOKEN_EXPIRY }, (data) => data.clientSecretExpiresAt
|
|
46
|
+
? data.clientSecretExpiresAt < Date.now()
|
|
47
|
+
: true);
|
|
48
|
+
});
|
|
49
|
+
exports.registerClient = registerClient;
|
|
50
|
+
const awsIdcHelpers = (clientCredentials, idc) => {
|
|
51
|
+
const { clientId, clientSecret } = clientCredentials;
|
|
52
|
+
const { id, region } = idc;
|
|
53
|
+
const buildOidcAuthorizeRequest = () => ({
|
|
54
|
+
init: {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
clientId,
|
|
58
|
+
clientSecret,
|
|
59
|
+
startUrl: `https://${id}.awsapps.com/start`,
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
url: `https://oidc.${region}.amazonaws.com/device_authorization`,
|
|
63
|
+
});
|
|
64
|
+
const buildIdcTokenRequest = (authorizeResponse) => ({
|
|
65
|
+
url: `https://oidc.${region}.amazonaws.com/token`,
|
|
66
|
+
init: {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
clientId,
|
|
70
|
+
clientSecret,
|
|
71
|
+
deviceCode: authorizeResponse.deviceCode,
|
|
72
|
+
grantType: login_1.DEVICE_GRANT_TYPE,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* Exchanges the oidc token for AWS credentials for a given account and permission set
|
|
78
|
+
* @param oidcResponse oidc token response fot he oidc /token endpoint
|
|
79
|
+
* @param request accountId and permissionSet to exchange for AWS credentials
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
82
|
+
const exchangeForAwsCredentials = (oidcResponse, request) => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
// There is a delay in between aws issuing the sso token and it being available for exchange for AWS credentials
|
|
84
|
+
// When exchanging token immediately, an "unauthorized" may will be thrown, so retry with sleep.
|
|
85
|
+
return yield (0, retry_1.retryWithSleep)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
const init = {
|
|
87
|
+
method: "GET",
|
|
88
|
+
headers: {
|
|
89
|
+
"x-amz-sso_bearer_token": oidcResponse.accessToken,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const { accountId, permissionSet } = request;
|
|
93
|
+
if (accountId === undefined)
|
|
94
|
+
throw new Error("Could not find an AWS account ID for this access request");
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
params.append("account_id", accountId);
|
|
97
|
+
params.append("role_name", permissionSet);
|
|
98
|
+
const response = yield fetch(`https://portal.sso.${region}.amazonaws.com/federation/credentials?${params.toString()}`, init);
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
throw new Error(`Failed to fetch AWS credentials: ${response.statusText}: ${yield response.text()}`);
|
|
101
|
+
return yield response.json();
|
|
102
|
+
}), () => true, 3);
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
loginSteps: {
|
|
106
|
+
providerType: "aws-oidc",
|
|
107
|
+
validateResponse: fetch_1.validateResponse,
|
|
108
|
+
buildAuthorizeRequest: buildOidcAuthorizeRequest,
|
|
109
|
+
buildTokenRequest: buildIdcTokenRequest,
|
|
110
|
+
processAuthzExpiry: (authorize) => ({
|
|
111
|
+
expires_in: authorize.expiresIn,
|
|
112
|
+
interval: authorize.interval,
|
|
113
|
+
}),
|
|
114
|
+
processAuthzResponse: (authorize) => ({
|
|
115
|
+
user_code: authorize.userCode,
|
|
116
|
+
verification_uri_complete: authorize.verificationUriComplete,
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
exchangeForAwsCredentials,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Returns AWS credentials for the specified account and permission set for the authorized user
|
|
124
|
+
* @param args accountId, permissionSet and idc to assume role associated with the permission set
|
|
125
|
+
* @returns
|
|
126
|
+
*/
|
|
127
|
+
const assumeRoleWithIdc = (args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
return yield (0, auth_1.cached)(`aws-idc-${args.accountId}-${args.permissionSet}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
const { idc } = args;
|
|
130
|
+
const { region } = idc;
|
|
131
|
+
const clientSecrets = yield (0, exports.registerClient)(region);
|
|
132
|
+
const { loginSteps, exchangeForAwsCredentials } = awsIdcHelpers(clientSecrets, idc);
|
|
133
|
+
const oidcResponse = yield (0, auth_1.cached)("aws-idc-device-authorization", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
const data = yield (0, login_1.oidcLogin)(loginSteps);
|
|
135
|
+
return Object.assign(Object.assign({}, data), { expiresAt: Date.now() + data.expiresIn * 1e3 });
|
|
136
|
+
}), { duration: AWS_TOKEN_EXPIRY }, (data) => (data.expiresAt ? data.expiresAt < Date.now() : true));
|
|
137
|
+
const credentials = yield exchangeForAwsCredentials(oidcResponse, {
|
|
138
|
+
accountId: args.accountId,
|
|
139
|
+
permissionSet: args.permissionSet,
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
AWS_ACCESS_KEY_ID: credentials.roleCredentials.accessKeyId,
|
|
143
|
+
AWS_SECRET_ACCESS_KEY: credentials.roleCredentials.secretAccessKey,
|
|
144
|
+
AWS_SESSION_TOKEN: credentials.roleCredentials.sessionToken,
|
|
145
|
+
AWS_SECURITY_TOKEN: credentials.roleCredentials.sessionToken,
|
|
146
|
+
expiresAt: credentials.roleCredentials.expiration,
|
|
147
|
+
};
|
|
148
|
+
}), { duration: AWS_TOKEN_EXPIRY }, (data) => (data.expiresAt ? data.expiresAt < Date.now() : true));
|
|
149
|
+
});
|
|
150
|
+
exports.assumeRoleWithIdc = assumeRoleWithIdc;
|
|
@@ -8,6 +8,6 @@ This file is part of @p0security/cli
|
|
|
8
8
|
|
|
9
9
|
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
10
10
|
**/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
export declare const
|
|
11
|
+
import { SshProvider } from "../../types/ssh";
|
|
12
|
+
import { AwsCredentials, AwsSshPermissionSpec, AwsSshRequest } from "./types";
|
|
13
|
+
export declare const awsSshProvider: SshProvider<AwsSshPermissionSpec, undefined, AwsSshRequest, AwsCredentials>;
|
package/dist/plugins/aws/ssh.js
CHANGED
|
@@ -1,14 +1,78 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
exports.awsSshProvider = void 0;
|
|
13
|
+
const util_1 = require("../../util");
|
|
14
|
+
const aws_1 = require("../okta/aws");
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
const idc_1 = require("./idc");
|
|
17
|
+
const install_1 = require("./ssm/install");
|
|
18
|
+
/** Maximum number of attempts to start an SSH session
|
|
19
|
+
*
|
|
20
|
+
* Each attempt consumes ~ 1 s.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_SSH_RETRIES = 30;
|
|
23
|
+
/** The name of the SessionManager port forwarding document. This document is managed by AWS. */
|
|
24
|
+
const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
25
|
+
exports.awsSshProvider = {
|
|
26
|
+
requestToSsh: (request) => {
|
|
27
|
+
const { permission, generated } = request;
|
|
28
|
+
const { instanceId, accountId, region } = permission.spec;
|
|
29
|
+
const { idc, ssh, name } = generated;
|
|
30
|
+
const { linuxUserName } = ssh;
|
|
31
|
+
const common = { linuxUserName, accountId, region, id: instanceId };
|
|
32
|
+
return !idc
|
|
33
|
+
? Object.assign(Object.assign({}, common), { role: name, type: "aws", access: "role" }) : Object.assign(Object.assign({}, common), { idc, permissionSet: name, type: "aws", access: "idc" });
|
|
34
|
+
},
|
|
35
|
+
toCliRequest: (request) => __awaiter(void 0, void 0, void 0, function* () { return (Object.assign(Object.assign({}, request), { cliLocalData: undefined })); }),
|
|
36
|
+
cloudProviderLogin: (authn, request) => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
|
+
var _a, _b, _c, _d;
|
|
38
|
+
if (!(yield (0, install_1.ensureSsmInstall)())) {
|
|
39
|
+
throw "Please try again after installing the required AWS utilities";
|
|
40
|
+
}
|
|
41
|
+
const { config } = yield (0, config_1.getAwsConfig)(authn, request.accountId);
|
|
42
|
+
if (!((_a = config.login) === null || _a === void 0 ? void 0 : _a.type) || ((_b = config.login) === null || _b === void 0 ? void 0 : _b.type) === "iam") {
|
|
43
|
+
throw "This account is not configured for SSH access via the P0 CLI";
|
|
44
|
+
}
|
|
45
|
+
return ((_c = config.login) === null || _c === void 0 ? void 0 : _c.type) === "idc"
|
|
46
|
+
? yield (0, idc_1.assumeRoleWithIdc)(request)
|
|
47
|
+
: ((_d = config.login) === null || _d === void 0 ? void 0 : _d.type) === "federated"
|
|
48
|
+
? yield (0, aws_1.assumeRoleWithOktaSaml)(authn, request)
|
|
49
|
+
: (0, util_1.throwAssertNever)(config.login);
|
|
50
|
+
}),
|
|
51
|
+
proxyCommand: (request) => {
|
|
52
|
+
return [
|
|
53
|
+
"aws",
|
|
54
|
+
"ssm",
|
|
55
|
+
"start-session",
|
|
56
|
+
"--region",
|
|
57
|
+
request.region,
|
|
58
|
+
"--target",
|
|
59
|
+
"%h",
|
|
60
|
+
"--document-name",
|
|
61
|
+
START_SSH_SESSION_DOCUMENT_NAME,
|
|
62
|
+
"--parameters",
|
|
63
|
+
'"portNumber=%p"',
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
reproCommands: (request) => {
|
|
67
|
+
// TODO: Add manual commands for IDC login
|
|
68
|
+
if (request.access !== "idc") {
|
|
69
|
+
return [
|
|
70
|
+
`eval $(p0 aws role assume ${request.role} --account ${request.accountId})`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
},
|
|
75
|
+
preTestAccessPropagationArgs: () => undefined,
|
|
76
|
+
maxRetries: MAX_SSH_RETRIES,
|
|
77
|
+
friendlyName: "AWS",
|
|
13
78
|
};
|
|
14
|
-
exports.awsRequestToSsh = awsRequestToSsh;
|
|
@@ -8,7 +8,8 @@ This file is part of @p0security/cli
|
|
|
8
8
|
|
|
9
9
|
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
10
10
|
**/
|
|
11
|
-
import {
|
|
11
|
+
import { PermissionSpec } from "../../types/request";
|
|
12
|
+
import { CliPermissionSpec } from "../../types/ssh";
|
|
12
13
|
import { CommonSshPermissionSpec } from "../ssh/types";
|
|
13
14
|
export declare type AwsCredentials = {
|
|
14
15
|
AWS_ACCESS_KEY_ID: string;
|
|
@@ -40,7 +41,7 @@ export declare type AwsFederatedLogin = {
|
|
|
40
41
|
};
|
|
41
42
|
};
|
|
42
43
|
};
|
|
43
|
-
declare type AwsLogin = AwsFederatedLogin | AwsIamLogin | AwsIdcLogin;
|
|
44
|
+
export declare type AwsLogin = AwsFederatedLogin | AwsIamLogin | AwsIdcLogin;
|
|
44
45
|
export declare type AwsItemConfig = {
|
|
45
46
|
label?: string;
|
|
46
47
|
state: string;
|
|
@@ -66,7 +67,30 @@ export declare type AwsSshGenerated = {
|
|
|
66
67
|
ssh: {
|
|
67
68
|
linuxUserName: string;
|
|
68
69
|
};
|
|
70
|
+
idc?: {
|
|
71
|
+
region: string;
|
|
72
|
+
id: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
export declare type AwsSshPermissionSpec = PermissionSpec<"ssh", AwsSshPermission, AwsSshGenerated>;
|
|
76
|
+
export declare type AwsSsh = CliPermissionSpec<AwsSshPermissionSpec, undefined>;
|
|
77
|
+
export declare type BaseAwsSshRequest = {
|
|
78
|
+
linuxUserName: string;
|
|
79
|
+
accountId: string;
|
|
80
|
+
region: string;
|
|
81
|
+
id: string;
|
|
82
|
+
type: "aws";
|
|
83
|
+
};
|
|
84
|
+
export declare type AwsSshRoleRequest = BaseAwsSshRequest & {
|
|
85
|
+
role: string;
|
|
86
|
+
access: "role";
|
|
87
|
+
};
|
|
88
|
+
export declare type AwsSshIdcRequest = BaseAwsSshRequest & {
|
|
89
|
+
permissionSet: string;
|
|
90
|
+
idc: {
|
|
91
|
+
id: string;
|
|
92
|
+
region: string;
|
|
93
|
+
};
|
|
94
|
+
access: "idc";
|
|
69
95
|
};
|
|
70
|
-
export declare type
|
|
71
|
-
export declare type AwsSsh = CliPermissionSpec<AwsPermissionSpec>;
|
|
72
|
-
export {};
|
|
96
|
+
export declare type AwsSshRequest = AwsSshIdcRequest | AwsSshRoleRequest;
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
8
|
-
|
|
9
|
-
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
10
|
-
**/
|
|
11
|
-
import { SshRequest } from "../../commands/shared";
|
|
12
|
-
import { GcpSsh } from "./types";
|
|
13
|
-
export declare const gcpRequestToSsh: (request: GcpSsh) => SshRequest;
|
|
1
|
+
import { SshProvider } from "../../types/ssh";
|
|
2
|
+
import { GcpSshPermissionSpec, GcpSshRequest } from "./types";
|
|
3
|
+
export declare const gcpSshProvider: SshProvider<GcpSshPermissionSpec, {
|
|
4
|
+
linuxUserName: string;
|
|
5
|
+
}, GcpSshRequest>;
|