@p0security/cli 0.4.1 → 0.5.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.
@@ -43,9 +43,9 @@ const mockPrint2 = stdio_1.print2;
43
43
  workflows: {
44
44
  items: [
45
45
  {
46
- identifier: "test-account",
47
46
  state: "installed",
48
47
  type: "aws",
48
+ identifier: "test-account",
49
49
  },
50
50
  ],
51
51
  },
@@ -64,6 +64,19 @@ describe("ssh", () => {
64
64
  id: "abcefg",
65
65
  isPreexisting: false,
66
66
  isPersistent,
67
+ event: {
68
+ permission: {
69
+ type: "session",
70
+ spec: {
71
+ resource: {
72
+ arn: "arn:aws:ec2:us-west-2:391052057035:instance/i-0b1b7b7b7b7b7b7b7",
73
+ },
74
+ },
75
+ },
76
+ generated: {
77
+ documentName: "documentName",
78
+ },
79
+ },
67
80
  });
68
81
  });
69
82
  it("should call p0 request with reason arg", () => __awaiter(void 0, void 0, void 0, function* () {
@@ -53,29 +53,33 @@ beforeEach(() => {
53
53
  describe("aws role", () => {
54
54
  describe("a single installed account", () => {
55
55
  const item = {
56
- account: {
57
- id: "1",
58
- description: "1 (test)",
59
- },
56
+ label: "test",
60
57
  state: "installed",
61
58
  };
62
59
  describe("without Okta SAML", () => {
63
- (0, firestore_1.mockGetDoc)({ workflows: { items: [item] } });
60
+ (0, firestore_1.mockGetDoc)({ "iam-write": { "1": item } });
64
61
  describe.each([
65
62
  ["ls", "aws role ls"],
66
63
  ["assume", "aws role assume Role1"],
67
64
  ])("%s", (_, command) => {
68
65
  it("should print a friendly error message", () => __awaiter(void 0, void 0, void 0, function* () {
69
66
  const error = yield (0, yargs_1.failure)((0, __1.awsCommand)((0, yargs_2.default)()), command);
70
- expect(error).toMatchInlineSnapshot(`"Account 1 (test) is not configured for Okta SAML login."`);
67
+ expect(error).toMatchInlineSnapshot(`"Account test is not configured for Okta SAML login."`);
71
68
  }));
72
69
  });
73
70
  });
74
71
  describe("with Okta SAML", () => {
75
72
  beforeEach(() => {
76
73
  (0, firestore_1.mockGetDoc)({
77
- workflows: {
78
- items: [Object.assign(Object.assign({}, item), { uidLocation: { id: "okta_saml_sso" } })],
74
+ "iam-write": {
75
+ "1": Object.assign(Object.assign({}, item), { login: {
76
+ type: "federated",
77
+ provider: {
78
+ type: "okta",
79
+ appId: "0oabcdefgh",
80
+ identityProvider: "okta",
81
+ },
82
+ } }),
79
83
  },
80
84
  });
81
85
  });
@@ -1,4 +1,4 @@
1
- import { AwsItemConfig, AwsOktaSamlUidLocation } from "../../plugins/aws/types";
1
+ import { AwsFederatedLogin } from "../../plugins/aws/types";
2
2
  import { Authn } from "../../types/identity";
3
3
  import yargs from "yargs";
4
4
  export declare const role: (yargs: yargs.Argv<{
@@ -15,8 +15,10 @@ export declare const role: (yargs: yargs.Argv<{
15
15
  */
16
16
  export declare const initOktaSaml: (authn: Authn, account: string | undefined) => Promise<{
17
17
  samlResponse: string;
18
- config: AwsItemConfig & {
19
- uidLocation: AwsOktaSamlUidLocation;
18
+ config: {
19
+ id: string;
20
+ } & import("../../plugins/aws/types").AwsItemConfig & {
21
+ login: AwsFederatedLogin;
20
22
  };
21
23
  account: string;
22
24
  }>;
@@ -42,21 +42,22 @@ const role = (yargs) => yargs.command("role", "Interact with AWS roles", (yargs)
42
42
  (0, firestore_1.guard)(oktaAwsAssumeRole))
43
43
  .demandCommand(1));
44
44
  exports.role = role;
45
- const isOktaSamlConfig = (config) => { var _a; return ((_a = config.uidLocation) === null || _a === void 0 ? void 0 : _a.id) === "okta_saml_sso"; };
45
+ const isFederatedLogin = (config) => { var _a; return ((_a = config.login) === null || _a === void 0 ? void 0 : _a.type) === "federated"; };
46
46
  /** Retrieves the configured Okta SAML response for the specified account
47
47
  *
48
48
  * If no account is passed, and the organization only has one account configured,
49
49
  * assumes that account.
50
50
  */
51
51
  const initOktaSaml = (authn, account) => __awaiter(void 0, void 0, void 0, function* () {
52
+ var _a;
52
53
  const { identity, config } = yield (0, config_1.getAwsConfig)(authn, account);
53
- if (!isOktaSamlConfig(config))
54
- throw `Account ${config.account.description} is not configured for Okta SAML login.`;
55
- const samlResponse = yield (0, login_1.getSamlResponse)(identity, config.uidLocation);
54
+ if (!isFederatedLogin(config))
55
+ throw `Account ${(_a = config.label) !== null && _a !== void 0 ? _a : config.id} is not configured for Okta SAML login.`;
56
+ const samlResponse = yield (0, login_1.getSamlResponse)(identity, config.login);
56
57
  return {
57
58
  samlResponse,
58
59
  config,
59
- account: config.account.id,
60
+ account: config.id,
60
61
  };
61
62
  });
62
63
  exports.initOktaSaml = initOktaSaml;
@@ -88,10 +89,10 @@ exports.rolesFromSaml = rolesFromSaml;
88
89
  * - The requested role is assigned to the user in Okta
89
90
  */
90
91
  const oktaAwsAssumeRole = (args) => __awaiter(void 0, void 0, void 0, function* () {
91
- var _a;
92
+ var _b;
92
93
  const authn = yield (0, auth_1.authenticate)();
93
94
  const awsCredential = yield (0, aws_1.assumeRoleWithOktaSaml)(authn, args);
94
- const isTty = (_a = typescript_1.sys.writeOutputIsTTY) === null || _a === void 0 ? void 0 : _a.call(typescript_1.sys);
95
+ const isTty = (_b = typescript_1.sys.writeOutputIsTTY) === null || _b === void 0 ? void 0 : _b.call(typescript_1.sys);
95
96
  if (isTty)
96
97
  (0, stdio_1.print2)("Execute the following commands:\n");
97
98
  const indent = isTty ? " " : "";
@@ -107,11 +108,11 @@ Or, populate these environment variables using BASH command substitution:
107
108
  });
108
109
  /** Lists assigned AWS roles for this user on this account */
109
110
  const oktaAwsListRoles = (args) => __awaiter(void 0, void 0, void 0, function* () {
110
- var _b;
111
+ var _c;
111
112
  const authn = yield (0, auth_1.authenticate)();
112
113
  const { account, samlResponse } = yield (0, exports.initOktaSaml)(authn, args.account);
113
114
  const { arns, roles } = (0, exports.rolesFromSaml)(account, samlResponse);
114
- const isTty = (_b = typescript_1.sys.writeOutputIsTTY) === null || _b === void 0 ? void 0 : _b.call(typescript_1.sys);
115
+ const isTty = (_c = typescript_1.sys.writeOutputIsTTY) === null || _c === void 0 ? void 0 : _c.call(typescript_1.sys);
115
116
  if (isTty)
116
117
  (0, stdio_1.print2)(`Your available roles for account ${account}:`);
117
118
  if (!(roles === null || roles === void 0 ? void 0 : roles.length)) {
@@ -4,9 +4,9 @@ import yargs from "yargs";
4
4
  export declare const requestCommand: (yargs: yargs.Argv<{}>) => yargs.Argv<{
5
5
  arguments: string[];
6
6
  }>;
7
- export declare const request: (args: yargs.ArgumentsCamelCase<{
7
+ export declare const request: <T>(args: yargs.ArgumentsCamelCase<{
8
8
  arguments: string[];
9
9
  wait?: boolean;
10
10
  }>, authn?: Authn, options?: {
11
11
  message?: "all" | "approval-required" | "none";
12
- }) => Promise<RequestResponse | undefined>;
12
+ }) => Promise<RequestResponse<T> | undefined>;
@@ -146,10 +146,22 @@ const ssh = (args) => __awaiter(void 0, void 0, void 0, function* () {
146
146
  (0, stdio_1.print2)("Did not receive access ID from server");
147
147
  return;
148
148
  }
149
- const { id, isPreexisting } = response;
149
+ const { id, isPreexisting, event } = response;
150
150
  if (!isPreexisting)
151
151
  (0, stdio_1.print2)("Waiting for access to be provisioned");
152
+ /**
153
+ * TODO TECH-DEBT ENG-1813:
154
+ * We use the id and waitForProvisioning to find the permission request document which has
155
+ * critical data, such as the document name and generated role, that we need to build up a
156
+ * viable SSM request.
157
+ *
158
+ * Replacing the permission with event.permission is necessary when trying to connect to an
159
+ * instance which has been granted approval through it's group. The event.permission object
160
+ * will contain details about the specific instance we are trying to connect to such as the
161
+ * instance id. Without an instance id, which an SSH group permission request document does
162
+ * not contain we cannot construct a valid SSM command.
163
+ */
152
164
  const requestData = yield waitForProvisioning(authn, id);
153
- const requestWithId = Object.assign(Object.assign({}, requestData), { id });
165
+ const requestWithId = Object.assign(Object.assign({}, requestData), { id, permission: event.permission });
154
166
  yield (0, ssm_1.ssm)(authn, requestWithId, args);
155
167
  });
@@ -1,5 +1,10 @@
1
1
  import { Authn } from "../../types/identity";
2
2
  export declare const getAwsConfig: (authn: Authn, account: string | undefined) => Promise<{
3
3
  identity: import("../../types/identity").Identity;
4
- config: import("./types").AwsItemConfig;
4
+ config: {
5
+ label?: string | undefined;
6
+ state: string;
7
+ login?: (import("./types").AwsIamLogin | import("./types").AwsIdcLogin | import("./types").AwsFederatedLogin) | undefined;
8
+ id: string;
9
+ };
5
10
  }>;
@@ -22,17 +22,23 @@ You should have received a copy of the GNU General Public License along with @p0
22
22
  **/
23
23
  const firestore_1 = require("../../drivers/firestore");
24
24
  const firestore_2 = require("firebase/firestore");
25
+ const lodash_1 = require("lodash");
25
26
  const getAwsConfig = (authn, account) => __awaiter(void 0, void 0, void 0, function* () {
26
- var _a, _b;
27
+ var _a;
27
28
  const { identity } = authn;
28
29
  const snapshot = yield (0, firestore_2.getDoc)((0, firestore_1.doc)(`o/${identity.org.tenantId}/integrations/aws`));
29
30
  const config = snapshot.data();
30
31
  // TODO: Support alias lookup
32
+ const allItems = (0, lodash_1.sortBy)(Object.entries((_a = config === null || config === void 0 ? void 0 : config["iam-write"]) !== null && _a !== void 0 ? _a : {}).filter(([, { state }]) => state === "installed"), ([id]) => id);
31
33
  const item = account
32
- ? (_a = config === null || config === void 0 ? void 0 : config.workflows) === null || _a === void 0 ? void 0 : _a.items.find((i) => i.state === "installed" && i.account.id === account)
33
- : (_b = config === null || config === void 0 ? void 0 : config.workflows) === null || _b === void 0 ? void 0 : _b.items[0];
34
+ ? allItems.find(([id, { label }]) => id === account || label === account)
35
+ : allItems.length !== 1
36
+ ? (() => {
37
+ throw `Please select a unique AWS account with --account; valid accounts are:\n${allItems.map(([id, { label }]) => label !== null && label !== void 0 ? label : id).join("\n")}`;
38
+ })()
39
+ : allItems[0];
34
40
  if (!item)
35
41
  throw `P0 is not installed on AWS account ${account}`;
36
- return { identity, config: item };
42
+ return { identity, config: Object.assign({ id: item[0] }, item[1]) };
37
43
  });
38
44
  exports.getAwsConfig = getAwsConfig;
@@ -236,7 +236,7 @@ const ssm = (authn, request, args) => __awaiter(void 0, void 0, void 0, function
236
236
  const isInstalled = yield (0, install_1.ensureSsmInstall)();
237
237
  if (!isInstalled)
238
238
  throw "Please try again after installing the required AWS utilities";
239
- const match = request.permission.spec.arn.match(INSTANCE_ARN_PATTERN);
239
+ const match = request.permission.spec.awsResourcePermission.resource.arn.match(INSTANCE_ARN_PATTERN);
240
240
  if (!match)
241
241
  throw "Did not receive a properly formatted instance identifier";
242
242
  const [, region, account, instance] = match;
@@ -13,37 +13,50 @@ export declare type AwsCredentials = {
13
13
  AWS_SECRET_ACCESS_KEY: string;
14
14
  AWS_SESSION_TOKEN: string;
15
15
  };
16
- export declare type AwsOktaSamlUidLocation = {
17
- id: "okta_saml_sso";
18
- samlProviderName: string;
19
- appId: string;
16
+ export declare type AwsIamLogin = {
17
+ type: "iam";
18
+ identity: {
19
+ type: "email";
20
+ } | {
21
+ type: "tag";
22
+ tagName: string;
23
+ };
20
24
  };
21
- declare type AwsUidLocation = AwsOktaSamlUidLocation | {
22
- id: "idc";
23
- parentId: string;
24
- } | {
25
- id: "user_tag";
26
- tagName: string;
27
- } | {
28
- id: "username";
25
+ export declare type AwsIdcLogin = {
26
+ type: "idc";
27
+ parent: string;
29
28
  };
30
- export declare type AwsItemConfig = {
31
- account: {
32
- id: string;
33
- description?: string;
29
+ export declare type AwsFederatedLogin = {
30
+ type: "federated";
31
+ provider: {
32
+ type: "okta";
33
+ appId: string;
34
+ identityProvider: string;
35
+ method: {
36
+ type: "saml";
37
+ };
34
38
  };
39
+ };
40
+ declare type AwsLogin = AwsFederatedLogin | AwsIamLogin | AwsIdcLogin;
41
+ export declare type AwsItemConfig = {
42
+ label?: string;
35
43
  state: string;
36
- uidLocation?: AwsUidLocation;
44
+ login?: AwsLogin;
37
45
  };
46
+ export declare type AwsItem = {
47
+ id: string;
48
+ } & AwsItemConfig;
38
49
  export declare type AwsConfig = {
39
- workflows?: {
40
- items: AwsItemConfig[];
41
- };
50
+ "iam-write": Record<string, AwsItemConfig>;
42
51
  };
43
52
  export declare type AwsSsh = {
44
53
  permission: {
45
54
  spec: {
46
- arn: string;
55
+ awsResourcePermission: {
56
+ resource: {
57
+ arn: string;
58
+ };
59
+ };
47
60
  };
48
61
  type: "session";
49
62
  };
@@ -33,7 +33,7 @@ const assumeRoleWithOktaSaml = (authn, args) => __awaiter(void 0, void 0, void 0
33
33
  account,
34
34
  role: args.role,
35
35
  saml: {
36
- providerName: config.uidLocation.samlProviderName,
36
+ providerName: config.login.provider.identityProvider,
37
37
  response: samlResponse,
38
38
  },
39
39
  });
@@ -1,8 +1,8 @@
1
1
  import { Identity } from "../../types/identity";
2
2
  import { TokenResponse } from "../../types/oidc";
3
3
  import { OrgData } from "../../types/org";
4
- import { AwsOktaSamlUidLocation } from "../aws/types";
4
+ import { AwsFederatedLogin } from "../aws/types";
5
5
  /** Logs in to Okta via OIDC */
6
6
  export declare const oktaLogin: (org: OrgData) => Promise<TokenResponse>;
7
7
  /** Retrieves a SAML response for an okta app */
8
- export declare const getSamlResponse: (identity: Identity, config: AwsOktaSamlUidLocation) => Promise<string>;
8
+ export declare const getSamlResponse: (identity: Identity, config: AwsFederatedLogin) => Promise<string>;
@@ -155,7 +155,7 @@ exports.oktaLogin = oktaLogin;
155
155
  /** Retrieves a SAML response for an okta app */
156
156
  // TODO: Inject Okta app
157
157
  const getSamlResponse = (identity, config) => __awaiter(void 0, void 0, void 0, function* () {
158
- const webTokenResponse = yield fetchSsoWebToken(config.appId, identity);
158
+ const webTokenResponse = yield fetchSsoWebToken(config.provider.appId, identity);
159
159
  const samlResponse = yield fetchSamlResponse(identity.org, webTokenResponse);
160
160
  if (!samlResponse) {
161
161
  throw "No SAML assertion obtained from Okta.";
@@ -26,10 +26,11 @@ export declare type Request<P extends PluginRequest = {
26
26
  permission: P["permission"];
27
27
  principal: string;
28
28
  };
29
- export declare type RequestResponse = {
29
+ export declare type RequestResponse<T> = {
30
30
  ok: true;
31
31
  message: string;
32
32
  id: string;
33
+ event: T;
33
34
  isPreexisting: boolean;
34
35
  isPersistent: boolean;
35
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p0security/cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Execute infra CLI commands with P0 grants",
5
5
  "main": "index.ts",
6
6
  "repository": {