@p0security/cli 0.8.2 → 0.9.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/dist/commands/__tests__/grant.test.d.ts +1 -0
- package/dist/commands/__tests__/grant.test.js +55 -0
- package/dist/commands/aws/files.d.ts +41 -0
- package/dist/commands/aws/files.js +108 -0
- package/dist/commands/grant.d.ts +4 -0
- package/dist/commands/grant.js +17 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/kubeconfig.d.ts +9 -0
- package/dist/commands/kubeconfig.js +190 -0
- package/dist/commands/request.d.ts +0 -8
- package/dist/commands/request.js +3 -102
- package/dist/commands/shared/index.d.ts +2 -2
- package/dist/commands/shared/index.js +1 -1
- package/dist/commands/shared/request.d.ts +14 -0
- package/dist/commands/shared/request.js +115 -0
- package/dist/commands/shared/ssh.d.ts +7 -1
- package/dist/commands/shared/ssh.js +9 -7
- package/dist/common/install.d.ts +11 -0
- package/dist/common/install.js +120 -0
- package/dist/drivers/stdio.d.ts +1 -0
- package/dist/drivers/stdio.js +1 -0
- package/dist/plugins/aws/ssh.d.ts +2 -2
- package/dist/plugins/aws/ssh.js +54 -0
- package/dist/plugins/aws/ssm/install.js +7 -86
- package/dist/plugins/google/ssh.d.ts +0 -10
- package/dist/plugins/google/ssh.js +45 -0
- package/dist/plugins/kubeconfig/index.d.ts +23 -0
- package/dist/plugins/kubeconfig/index.js +98 -0
- package/dist/plugins/kubeconfig/install.d.ts +1 -0
- package/dist/plugins/kubeconfig/install.js +65 -0
- package/dist/plugins/kubeconfig/types.d.ts +57 -0
- package/dist/plugins/kubeconfig/types.js +2 -0
- package/dist/plugins/ssh/index.d.ts +2 -2
- package/dist/plugins/ssh/index.js +65 -93
- package/dist/types/request.d.ts +2 -1
- package/dist/types/ssh.d.ts +21 -1
- package/dist/util.d.ts +11 -0
- package/dist/util.js +13 -1
- package/package.json +7 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
/** Copyright © 2024-present P0 Security
|
|
16
|
+
|
|
17
|
+
This file is part of @p0security/cli
|
|
18
|
+
|
|
19
|
+
@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.
|
|
20
|
+
|
|
21
|
+
@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.
|
|
22
|
+
|
|
23
|
+
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
24
|
+
**/
|
|
25
|
+
const api_1 = require("../../drivers/api");
|
|
26
|
+
const stdio_1 = require("../../drivers/stdio");
|
|
27
|
+
const grant_1 = require("../grant");
|
|
28
|
+
const yargs_1 = __importDefault(require("yargs"));
|
|
29
|
+
jest.mock("../../drivers/api");
|
|
30
|
+
jest.mock("../../drivers/auth");
|
|
31
|
+
jest.mock("../../drivers/stdio");
|
|
32
|
+
const mockFetchCommand = api_1.fetchCommand;
|
|
33
|
+
const mockPrint1 = stdio_1.print1;
|
|
34
|
+
const mockPrint2 = stdio_1.print2;
|
|
35
|
+
describe("grant", () => {
|
|
36
|
+
beforeEach(() => jest.clearAllMocks());
|
|
37
|
+
describe("when valid grant command", () => {
|
|
38
|
+
const command = "grant gcloud role viewer --to someone@test.com --principal-type user";
|
|
39
|
+
function mockFetch() {
|
|
40
|
+
mockFetchCommand.mockResolvedValue({
|
|
41
|
+
ok: true,
|
|
42
|
+
message: "a message",
|
|
43
|
+
id: "abcefg",
|
|
44
|
+
isPreexisting: false,
|
|
45
|
+
isPersistent: false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
it(`should print request response`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
mockFetch();
|
|
50
|
+
yield (0, grant_1.grantCommand)((0, yargs_1.default)()).parse(command);
|
|
51
|
+
expect(mockPrint2.mock.calls).toMatchSnapshot();
|
|
52
|
+
expect(mockPrint1).not.toHaveBeenCalled();
|
|
53
|
+
}));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Copyright © 2024-present P0 Security
|
|
2
|
+
|
|
3
|
+
This file is part of @p0security/cli
|
|
4
|
+
|
|
5
|
+
@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.
|
|
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 { AwsCredentials } from "../../plugins/aws/types";
|
|
12
|
+
import * as ini from "ini";
|
|
13
|
+
export declare const AWS_CONFIG_FILE: string;
|
|
14
|
+
export declare const AWS_CREDENTIALS_FILE: string;
|
|
15
|
+
/**
|
|
16
|
+
* Reads in an AWS CLI configuration file, which is formatted as INI text, and
|
|
17
|
+
* returns an arbitrary object representing the contents.
|
|
18
|
+
*
|
|
19
|
+
* @param path Path of the file to read
|
|
20
|
+
* @returns Arbitrary object representing the contents of the file, or an empty
|
|
21
|
+
* object if the file is empty or does not exist
|
|
22
|
+
*/
|
|
23
|
+
export declare const readIniFile: (path: string) => Promise<{
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* This function writes an arbitrary object as INI-formatted text to a file
|
|
28
|
+
* atomically by first writing the data to a temporary file then moving the
|
|
29
|
+
* temporary file on top of the target file. This minimizes the chance that an
|
|
30
|
+
* exception, system crash, or other similar event will leave the file in a
|
|
31
|
+
* corrupted state; this is important since we're mucking around with the AWS
|
|
32
|
+
* CLI's configuration files.
|
|
33
|
+
*
|
|
34
|
+
* @param path Path of the (permanent) file to write to
|
|
35
|
+
* @param obj Arbitrary object to convert to INI-formatted text and write to the
|
|
36
|
+
* file
|
|
37
|
+
* @param iniEncodeOptions Options to pass to the INI encoding library
|
|
38
|
+
*/
|
|
39
|
+
export declare const atomicWriteIniFile: (path: string, obj: any, iniEncodeOptions?: ini.EncodeOptions) => Promise<void>;
|
|
40
|
+
export declare const writeAwsTempCredentials: (profileName: string, awsCredentials: AwsCredentials) => Promise<void>;
|
|
41
|
+
export declare const writeAwsConfigProfile: (profileName: string, profileConfig: any) => Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.writeAwsConfigProfile = exports.writeAwsTempCredentials = exports.atomicWriteIniFile = exports.readIniFile = exports.AWS_CREDENTIALS_FILE = exports.AWS_CONFIG_FILE = void 0;
|
|
39
|
+
const ini = __importStar(require("ini"));
|
|
40
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
43
|
+
const tmp_promise_1 = __importDefault(require("tmp-promise"));
|
|
44
|
+
const AWS_CONFIG_PATH = node_path_1.default.join(os.homedir(), ".aws");
|
|
45
|
+
exports.AWS_CONFIG_FILE = node_path_1.default.join(AWS_CONFIG_PATH, "config");
|
|
46
|
+
exports.AWS_CREDENTIALS_FILE = node_path_1.default.join(AWS_CONFIG_PATH, "credentials");
|
|
47
|
+
// Reference documentation: https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html
|
|
48
|
+
/**
|
|
49
|
+
* Reads in an AWS CLI configuration file, which is formatted as INI text, and
|
|
50
|
+
* returns an arbitrary object representing the contents.
|
|
51
|
+
*
|
|
52
|
+
* @param path Path of the file to read
|
|
53
|
+
* @returns Arbitrary object representing the contents of the file, or an empty
|
|
54
|
+
* object if the file is empty or does not exist
|
|
55
|
+
*/
|
|
56
|
+
const readIniFile = (path) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
try {
|
|
58
|
+
const data = yield fs.readFile(path, { encoding: "utf-8" });
|
|
59
|
+
return data ? ini.parse(data) : {};
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code === "ENOENT") {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
exports.readIniFile = readIniFile;
|
|
69
|
+
/**
|
|
70
|
+
* This function writes an arbitrary object as INI-formatted text to a file
|
|
71
|
+
* atomically by first writing the data to a temporary file then moving the
|
|
72
|
+
* temporary file on top of the target file. This minimizes the chance that an
|
|
73
|
+
* exception, system crash, or other similar event will leave the file in a
|
|
74
|
+
* corrupted state; this is important since we're mucking around with the AWS
|
|
75
|
+
* CLI's configuration files.
|
|
76
|
+
*
|
|
77
|
+
* @param path Path of the (permanent) file to write to
|
|
78
|
+
* @param obj Arbitrary object to convert to INI-formatted text and write to the
|
|
79
|
+
* file
|
|
80
|
+
* @param iniEncodeOptions Options to pass to the INI encoding library
|
|
81
|
+
*/
|
|
82
|
+
const atomicWriteIniFile = (path, obj, iniEncodeOptions) => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
const data = ini.stringify(obj, iniEncodeOptions);
|
|
84
|
+
// Permissions will be moved along with the file
|
|
85
|
+
const { path: tmpPath } = yield tmp_promise_1.default.file({ mode: 0o600, prefix: "p0cli-" });
|
|
86
|
+
yield fs.writeFile(tmpPath, data, { encoding: "utf-8" });
|
|
87
|
+
yield fs.rename(tmpPath, path);
|
|
88
|
+
});
|
|
89
|
+
exports.atomicWriteIniFile = atomicWriteIniFile;
|
|
90
|
+
const writeAwsTempCredentials = (profileName, awsCredentials) => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
const credentials = yield (0, exports.readIniFile)(exports.AWS_CREDENTIALS_FILE);
|
|
92
|
+
credentials[profileName] = {
|
|
93
|
+
aws_access_key_id: awsCredentials.AWS_ACCESS_KEY_ID,
|
|
94
|
+
aws_secret_access_key: awsCredentials.AWS_SECRET_ACCESS_KEY,
|
|
95
|
+
aws_session_token: awsCredentials.AWS_SESSION_TOKEN,
|
|
96
|
+
};
|
|
97
|
+
// The credentials file is formatted with whitespace before and after the `=`
|
|
98
|
+
yield (0, exports.atomicWriteIniFile)(exports.AWS_CREDENTIALS_FILE, credentials, {
|
|
99
|
+
whitespace: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
exports.writeAwsTempCredentials = writeAwsTempCredentials;
|
|
103
|
+
const writeAwsConfigProfile = (profileName, profileConfig) => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
const config = yield (0, exports.readIniFile)(exports.AWS_CONFIG_FILE);
|
|
105
|
+
config[`profile ${profileName}`] = profileConfig;
|
|
106
|
+
yield (0, exports.atomicWriteIniFile)(exports.AWS_CONFIG_FILE, config);
|
|
107
|
+
});
|
|
108
|
+
exports.writeAwsConfigProfile = writeAwsConfigProfile;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.grantCommand = void 0;
|
|
4
|
+
/** Copyright © 2024-present P0 Security
|
|
5
|
+
|
|
6
|
+
This file is part of @p0security/cli
|
|
7
|
+
|
|
8
|
+
@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.
|
|
9
|
+
|
|
10
|
+
@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.
|
|
11
|
+
|
|
12
|
+
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
13
|
+
**/
|
|
14
|
+
const firestore_1 = require("../drivers/firestore");
|
|
15
|
+
const request_1 = require("./shared/request");
|
|
16
|
+
const grantCommand = (yargs) => yargs.command("grant [arguments..]", "Grant access to another identity", request_1.requestArgs, (0, firestore_1.guard)((0, request_1.request)("grant")));
|
|
17
|
+
exports.grantCommand = grantCommand;
|
package/dist/commands/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const stdio_1 = require("../drivers/stdio");
|
|
|
18
18
|
const version_1 = require("../middlewares/version");
|
|
19
19
|
const allow_1 = require("./allow");
|
|
20
20
|
const aws_1 = require("./aws");
|
|
21
|
+
const grant_1 = require("./grant");
|
|
22
|
+
const kubeconfig_1 = require("./kubeconfig");
|
|
21
23
|
const login_1 = require("./login");
|
|
22
24
|
const ls_1 = require("./ls");
|
|
23
25
|
const request_1 = require("./request");
|
|
@@ -28,12 +30,14 @@ const yargs_1 = __importDefault(require("yargs"));
|
|
|
28
30
|
const helpers_1 = require("yargs/helpers");
|
|
29
31
|
const commands = [
|
|
30
32
|
aws_1.awsCommand,
|
|
33
|
+
grant_1.grantCommand,
|
|
31
34
|
login_1.loginCommand,
|
|
32
35
|
ls_1.lsCommand,
|
|
33
36
|
request_1.requestCommand,
|
|
34
37
|
allow_1.allowCommand,
|
|
35
38
|
ssh_1.sshCommand,
|
|
36
39
|
scp_1.scpCommand,
|
|
40
|
+
kubeconfig_1.kubeconfigCommand,
|
|
37
41
|
];
|
|
38
42
|
exports.cli = commands
|
|
39
43
|
.reduce((m, c) => c(m), (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import yargs from "yargs";
|
|
2
|
+
export declare type KubeconfigCommandArgs = {
|
|
3
|
+
cluster: string;
|
|
4
|
+
role: string;
|
|
5
|
+
resource?: string;
|
|
6
|
+
reason?: string;
|
|
7
|
+
requestedDuration?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const kubeconfigCommand: (yargs: yargs.Argv<{}>) => yargs.Argv<KubeconfigCommandArgs>;
|
|
@@ -0,0 +1,190 @@
|
|
|
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.kubeconfigCommand = 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 retry_1 = require("../common/retry");
|
|
24
|
+
const auth_1 = require("../drivers/auth");
|
|
25
|
+
const firestore_1 = require("../drivers/firestore");
|
|
26
|
+
const stdio_1 = require("../drivers/stdio");
|
|
27
|
+
const kubeconfig_1 = require("../plugins/kubeconfig");
|
|
28
|
+
const install_1 = require("../plugins/kubeconfig/install");
|
|
29
|
+
const util_1 = require("../util");
|
|
30
|
+
const files_1 = require("./aws/files");
|
|
31
|
+
const kubeconfigCommand = (yargs) => yargs.command("kubeconfig", "Request access to and automatically configure kubectl for a k8s cluster hosted by a cloud provider. Currently supports AWS EKS only.", (yargs) => yargs
|
|
32
|
+
.option("cluster", {
|
|
33
|
+
type: "string",
|
|
34
|
+
demandOption: true,
|
|
35
|
+
describe: "The ID of the k8s cluster as configured P0 Security",
|
|
36
|
+
})
|
|
37
|
+
.option("resource", {
|
|
38
|
+
type: "string",
|
|
39
|
+
describe: 'The resource or resource type (e.g., "Pod / *"), or omit for all',
|
|
40
|
+
})
|
|
41
|
+
.option("role", {
|
|
42
|
+
type: "string",
|
|
43
|
+
demandOption: true,
|
|
44
|
+
describe: 'The k8s role to request, e.g., "ClusterRole / cluster-admin"',
|
|
45
|
+
})
|
|
46
|
+
.option("reason", {
|
|
47
|
+
type: "string",
|
|
48
|
+
describe: "Reason access is needed",
|
|
49
|
+
})
|
|
50
|
+
.option("requested-duration", {
|
|
51
|
+
type: "string",
|
|
52
|
+
// Copied from the P0 backend
|
|
53
|
+
describe: "Requested duration for access (format like '10 minutes', '2 hours', '5 days', or '1 week')",
|
|
54
|
+
}), (0, firestore_1.guard)(kubeconfigAction));
|
|
55
|
+
exports.kubeconfigCommand = kubeconfigCommand;
|
|
56
|
+
const kubeconfigAction = (args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
const role = normalizeRoleArg(args.role);
|
|
58
|
+
if (args.resource) {
|
|
59
|
+
validateResourceArg(args.resource);
|
|
60
|
+
}
|
|
61
|
+
const authn = yield (0, auth_1.authenticate)();
|
|
62
|
+
const { clusterConfig, awsLoginType } = yield (0, kubeconfig_1.getAndValidateK8sIntegration)(authn, args.cluster);
|
|
63
|
+
const { clusterId, awsAccountId, awsClusterArn } = clusterConfig;
|
|
64
|
+
if (!(yield (0, install_1.ensureEksInstall)())) {
|
|
65
|
+
throw "Required dependencies are missing; please try again after installing them, or check that they are available on the PATH.";
|
|
66
|
+
}
|
|
67
|
+
const request = yield (0, kubeconfig_1.requestAccessToCluster)(authn, args, clusterId, role);
|
|
68
|
+
const awsAuth = yield (0, kubeconfig_1.awsCloudAuth)(authn, awsAccountId, request.generated, awsLoginType);
|
|
69
|
+
const profile = (0, kubeconfig_1.profileName)(clusterId);
|
|
70
|
+
// The `aws eks update-kubeconfig` command can't handle the ARN of the EKS cluster.
|
|
71
|
+
// So we must, with great annoyance, parse it to extract the cluster name and region.
|
|
72
|
+
const clusterInfo = extractClusterNameAndRegion(awsClusterArn);
|
|
73
|
+
const { clusterRegion, clusterName } = clusterInfo;
|
|
74
|
+
yield (0, files_1.writeAwsTempCredentials)(profile, awsAuth);
|
|
75
|
+
yield (0, files_1.writeAwsConfigProfile)(profile, { region: clusterRegion });
|
|
76
|
+
const updateKubeconfigArgs = [
|
|
77
|
+
"eks",
|
|
78
|
+
"update-kubeconfig",
|
|
79
|
+
"--name",
|
|
80
|
+
clusterName,
|
|
81
|
+
"--region",
|
|
82
|
+
clusterRegion,
|
|
83
|
+
"--profile",
|
|
84
|
+
profile,
|
|
85
|
+
];
|
|
86
|
+
try {
|
|
87
|
+
// Federated access especially sometimes takes some time to propagate, so
|
|
88
|
+
// retry for up to 20 seconds just in case it takes a while.
|
|
89
|
+
const awsResult = yield (0, retry_1.retryWithSleep)(() => __awaiter(void 0, void 0, void 0, function* () { return yield (0, util_1.exec)("aws", updateKubeconfigArgs, { check: true }); }), () => true, 8, 2500);
|
|
90
|
+
(0, stdio_1.print2)(awsResult.stdout);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
(0, stdio_1.print2)("Failed to invoke `aws eks update-kubeconfig`");
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
// `aws update-kubeconfig` will set the kubectl context if it made a change to the kubeconfig file.
|
|
97
|
+
// We'll set the context manually anyway, just in case. `aws update-kubeconfig` names the context
|
|
98
|
+
// with the EKS cluster's ARN.
|
|
99
|
+
try {
|
|
100
|
+
const kubectlResult = yield (0, util_1.exec)("kubectl", ["config", "use-context", awsClusterArn], { check: true });
|
|
101
|
+
(0, stdio_1.print2)(kubectlResult.stdout);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
(0, stdio_1.print2)("Failed to invoke `kubectl config use-context`");
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
(0, stdio_1.print2)("Access granted and kubectl configured successfully. Re-run this command to refresh access if credentials expire.");
|
|
108
|
+
if (process.env.AWS_ACCESS_KEY_ID) {
|
|
109
|
+
(0, stdio_1.print2)(`${stdio_1.Ansi.Yellow}Warning: AWS credentials were detected in your environment, which may cause kubectl errors. ` +
|
|
110
|
+
`To avoid issues, unset with \`unset AWS_ACCESS_KEY_ID\`.${stdio_1.Ansi.Reset}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* Normalize the role argument to the format expected by the P0 backend,
|
|
115
|
+
* matching the way the Slack modal formats the role. Also validates that the
|
|
116
|
+
* role argument contains the components expected by the backend without having
|
|
117
|
+
* to make the request first.
|
|
118
|
+
*
|
|
119
|
+
* Currently, the P0 backend does not validate request arguments until after a
|
|
120
|
+
* request is approved; this function allows the validation to be done up-front
|
|
121
|
+
* pending a future backend change. TODO: ENG-2365.
|
|
122
|
+
*
|
|
123
|
+
* @param role The role argument to normalize
|
|
124
|
+
* @returns The normalized role value to pass to the backend
|
|
125
|
+
*/
|
|
126
|
+
const normalizeRoleArg = (role) => {
|
|
127
|
+
const SEPARATOR = "/";
|
|
128
|
+
const SYNTAX_HINT = "The role argument must be in one of the following formats:\n" +
|
|
129
|
+
"- ClusterRole/<roleName>\n" +
|
|
130
|
+
"- CuratedRole/<roleName>\n" +
|
|
131
|
+
"- Role/<namespace>/<roleName>";
|
|
132
|
+
const items = role.split(SEPARATOR).map((item) => item.trim());
|
|
133
|
+
if (items.length < 2 || items.length > 3) {
|
|
134
|
+
throw `Invalid format for role argument.\n${SYNTAX_HINT}`;
|
|
135
|
+
}
|
|
136
|
+
if (!items[0]) {
|
|
137
|
+
throw `Role kind must be specified.\n${SYNTAX_HINT}`;
|
|
138
|
+
}
|
|
139
|
+
if ((0, util_1.ciEquals)(items[0], "ClusterRole")) {
|
|
140
|
+
return `ClusterRole ${SEPARATOR} ${items[1]}`;
|
|
141
|
+
}
|
|
142
|
+
else if ((0, util_1.ciEquals)(items[0], "CuratedRole")) {
|
|
143
|
+
return `CuratedRole ${SEPARATOR} ${items[1]}`;
|
|
144
|
+
}
|
|
145
|
+
else if ((0, util_1.ciEquals)(items[0], "Role")) {
|
|
146
|
+
if (items.length !== 3) {
|
|
147
|
+
throw `Invalid format for role argument.\n${SYNTAX_HINT}`;
|
|
148
|
+
}
|
|
149
|
+
return `Role ${SEPARATOR} ${items[1]} ${SEPARATOR} ${items[2]}`;
|
|
150
|
+
}
|
|
151
|
+
throw `Invalid role kind ${items[0]}.\n${SYNTAX_HINT}`;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Validate that the resource argument is of the format expected by the P0
|
|
155
|
+
* backend, again matching the way the Slack modal formats the resource.
|
|
156
|
+
*
|
|
157
|
+
* Currently, the P0 backend does not validate request arguments until after a
|
|
158
|
+
* request is approved; this function allows the validation to be done up-front
|
|
159
|
+
* pending a future backend change. TODO: ENG-2365.
|
|
160
|
+
*
|
|
161
|
+
* @param resource The resource argument to validate
|
|
162
|
+
*/
|
|
163
|
+
const validateResourceArg = (resource) => {
|
|
164
|
+
const SEPARATOR = " / ";
|
|
165
|
+
const items = resource.split(SEPARATOR);
|
|
166
|
+
if (items.length < 2 || items.length > 3) {
|
|
167
|
+
throw ("Invalid format for resource argument.\n" +
|
|
168
|
+
"The resource argument must be in one of the following formats (spaces required):\n" +
|
|
169
|
+
"- <kind> / <namespace> / <name>\n" +
|
|
170
|
+
"- <kind> / <name>");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const extractClusterNameAndRegion = (clusterArn) => {
|
|
174
|
+
const INVALID_ARN_MSG = `Invalid EKS cluster ARN: ${clusterArn}`;
|
|
175
|
+
// Example EKS cluster ARN: arn:aws:eks:us-west-2:123456789012:cluster/my-testing-cluster
|
|
176
|
+
const parts = clusterArn.split(":");
|
|
177
|
+
if (parts.length < 6 || !parts[3] || !parts[5]) {
|
|
178
|
+
throw INVALID_ARN_MSG;
|
|
179
|
+
}
|
|
180
|
+
const clusterRegion = parts[3];
|
|
181
|
+
const resource = parts[5].split("/");
|
|
182
|
+
if (resource[0] !== "cluster") {
|
|
183
|
+
throw INVALID_ARN_MSG;
|
|
184
|
+
}
|
|
185
|
+
const clusterName = resource[1];
|
|
186
|
+
if (!clusterName) {
|
|
187
|
+
throw INVALID_ARN_MSG;
|
|
188
|
+
}
|
|
189
|
+
return { clusterRegion, clusterName };
|
|
190
|
+
};
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import { Authn } from "../types/identity";
|
|
2
|
-
import { RequestResponse } from "../types/request";
|
|
3
1
|
import yargs from "yargs";
|
|
4
2
|
export declare const requestCommand: (yargs: yargs.Argv<{}>) => yargs.Argv<{
|
|
5
3
|
arguments: string[];
|
|
6
4
|
}>;
|
|
7
|
-
export declare const request: <T>(args: yargs.ArgumentsCamelCase<{
|
|
8
|
-
arguments: string[];
|
|
9
|
-
wait?: boolean;
|
|
10
|
-
}>, authn?: Authn, options?: {
|
|
11
|
-
message?: "all" | "approval-required" | "none";
|
|
12
|
-
}) => Promise<RequestResponse<T> | undefined>;
|
package/dist/commands/request.js
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
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
|
-
};
|
|
11
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
3
|
+
exports.requestCommand = void 0;
|
|
13
4
|
/** Copyright © 2024-present P0 Security
|
|
14
5
|
|
|
15
6
|
This file is part of @p0security/cli
|
|
@@ -20,97 +11,7 @@ This file is part of @p0security/cli
|
|
|
20
11
|
|
|
21
12
|
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
13
|
**/
|
|
23
|
-
const api_1 = require("../drivers/api");
|
|
24
|
-
const auth_1 = require("../drivers/auth");
|
|
25
14
|
const firestore_1 = require("../drivers/firestore");
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const typescript_1 = require("typescript");
|
|
29
|
-
const WAIT_TIMEOUT = 300e3;
|
|
30
|
-
const APPROVED = { message: "Your request was approved", code: 0 };
|
|
31
|
-
const DENIED = { message: "Your request was denied", code: 2 };
|
|
32
|
-
const ERRORED = { message: "Your request encountered an error", code: 1 };
|
|
33
|
-
const COMPLETED_REQUEST_STATUSES = {
|
|
34
|
-
APPROVED,
|
|
35
|
-
APPROVED_NOTIFIED: APPROVED,
|
|
36
|
-
DONE: APPROVED,
|
|
37
|
-
DONE_NOTIFIED: APPROVED,
|
|
38
|
-
DENIED,
|
|
39
|
-
ERRORED,
|
|
40
|
-
};
|
|
41
|
-
const isCompletedStatus = (status) => status in COMPLETED_REQUEST_STATUSES;
|
|
42
|
-
const requestArgs = (yargs) => yargs
|
|
43
|
-
.parserConfiguration({ "unknown-options-as-args": true })
|
|
44
|
-
.help(false) // Turn off help in order to forward the --help command to the backend so P0 can provide the available requestable resources
|
|
45
|
-
.option("wait", {
|
|
46
|
-
alias: "w",
|
|
47
|
-
boolean: true,
|
|
48
|
-
default: false,
|
|
49
|
-
describe: "Block until the command is completed",
|
|
50
|
-
})
|
|
51
|
-
.option("arguments", {
|
|
52
|
-
array: true,
|
|
53
|
-
string: true,
|
|
54
|
-
default: [],
|
|
55
|
-
});
|
|
56
|
-
const requestCommand = (yargs) => yargs.command("request [arguments..]", "Manually request permissions on a resource", requestArgs, (0, firestore_1.guard)(exports.request));
|
|
15
|
+
const request_1 = require("./shared/request");
|
|
16
|
+
const requestCommand = (yargs) => yargs.command("request [arguments..]", "Manually request permissions on a resource", request_1.requestArgs, (0, firestore_1.guard)((0, request_1.request)("request")));
|
|
57
17
|
exports.requestCommand = requestCommand;
|
|
58
|
-
const waitForRequest = (tenantId, requestId, logMessage) => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
-
return yield new Promise((resolve) => {
|
|
60
|
-
if (logMessage)
|
|
61
|
-
(0, stdio_1.print2)("Will wait up to 5 minutes for this request to complete...");
|
|
62
|
-
let cancel = undefined;
|
|
63
|
-
const unsubscribe = (0, firestore_2.onSnapshot)((0, firestore_1.doc)(`o/${tenantId}/permission-requests/${requestId}`), (snap) => {
|
|
64
|
-
const data = snap.data();
|
|
65
|
-
if (!data)
|
|
66
|
-
return;
|
|
67
|
-
const { status } = data;
|
|
68
|
-
if (isCompletedStatus(status)) {
|
|
69
|
-
if (cancel)
|
|
70
|
-
clearTimeout(cancel);
|
|
71
|
-
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
72
|
-
const { message, code } = COMPLETED_REQUEST_STATUSES[status];
|
|
73
|
-
if (code !== 0 || logMessage)
|
|
74
|
-
(0, stdio_1.print2)(message);
|
|
75
|
-
resolve(code);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
cancel = setTimeout(() => {
|
|
79
|
-
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
80
|
-
(0, stdio_1.print2)("Your request did not complete within 5 minutes.");
|
|
81
|
-
resolve(4);
|
|
82
|
-
}, WAIT_TIMEOUT);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
const request = (args, authn, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
-
const resolvedAuthn = authn !== null && authn !== void 0 ? authn : (yield (0, auth_1.authenticate)());
|
|
87
|
-
const { userCredential } = resolvedAuthn;
|
|
88
|
-
const data = yield (0, api_1.fetchCommand)(resolvedAuthn, args, [
|
|
89
|
-
"request",
|
|
90
|
-
...args.arguments,
|
|
91
|
-
]);
|
|
92
|
-
if (data && "ok" in data && "message" in data && data.ok) {
|
|
93
|
-
const logMessage = !(options === null || options === void 0 ? void 0 : options.message) ||
|
|
94
|
-
(options === null || options === void 0 ? void 0 : options.message) === "all" ||
|
|
95
|
-
((options === null || options === void 0 ? void 0 : options.message) === "approval-required" &&
|
|
96
|
-
!data.isPreexisting &&
|
|
97
|
-
!data.isPersistent);
|
|
98
|
-
if (logMessage)
|
|
99
|
-
(0, stdio_1.print2)(data.message);
|
|
100
|
-
const { id } = data;
|
|
101
|
-
if (args.wait && id && userCredential.user.tenantId) {
|
|
102
|
-
const code = yield waitForRequest(userCredential.user.tenantId, id, logMessage);
|
|
103
|
-
if (code) {
|
|
104
|
-
typescript_1.sys.exit(code);
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
return data;
|
|
108
|
-
}
|
|
109
|
-
else
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
throw data;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
exports.request = request;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Authn } from "../../types/identity";
|
|
2
|
-
import { Request } from "../../types/request";
|
|
2
|
+
import { PluginRequest, Request } from "../../types/request";
|
|
3
3
|
/** Waits until P0 grants access for a request */
|
|
4
|
-
export declare const waitForProvisioning: <P extends
|
|
4
|
+
export declare const waitForProvisioning: <P extends PluginRequest>(authn: Authn, requestId: string) => Promise<Request<P>>;
|
|
@@ -57,7 +57,7 @@ const waitForProvisioning = (authn, requestId) => __awaiter(void 0, void 0, void
|
|
|
57
57
|
cancel = setTimeout(() => {
|
|
58
58
|
if (!isResolved) {
|
|
59
59
|
unsubscribe();
|
|
60
|
-
reject("Timeout awaiting
|
|
60
|
+
reject("Timeout awaiting access grant. Please try again.");
|
|
61
61
|
}
|
|
62
62
|
}, GRANT_TIMEOUT_MILLIS);
|
|
63
63
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Authn } from "../../types/identity";
|
|
2
|
+
import { RequestResponse } from "../../types/request";
|
|
3
|
+
import yargs from "yargs";
|
|
4
|
+
export declare const requestArgs: <T>(yargs: yargs.Argv<T>) => yargs.Argv<T & {
|
|
5
|
+
wait: boolean;
|
|
6
|
+
} & {
|
|
7
|
+
arguments: string[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare const request: (command: "grant" | "request") => <T>(args: yargs.ArgumentsCamelCase<{
|
|
10
|
+
arguments: string[];
|
|
11
|
+
wait?: boolean;
|
|
12
|
+
}>, authn?: Authn, options?: {
|
|
13
|
+
message?: "all" | "approval-required" | "none";
|
|
14
|
+
}) => Promise<RequestResponse<T> | undefined>;
|