Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENG-2884 Add multi-host support #132

Merged
merged 6 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
},
"scripts": {
"build": "tsc && cp -r public dist/",
"test:unit": "NODE_ENV=unit jest --color",
"clean": "rm -f tsconfig.tsbuildinfo && rm -rf dist/",
"lint": "yarn prettier --check . && yarn run eslint --max-warnings 0 .",
"p0": "node --no-deprecation ./p0",
Expand Down
9 changes: 8 additions & 1 deletion src/commands/__tests__/__snapshots__/login.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`login organization exists should write the user's identity to the file system 1`] = `
exports[`login organization exists should write the user's identity & config to the file system 1`] = `
[
[
"/Users/fabianjoya/.p0/config.json",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is won't pass on other computers because it has the home folder in it. I think jest has some way of asserting on a pattern instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"{"fs":{"apiKey":"AIzaSyC9A5VXSwDDS-Vp4WH_UIanEqJvv_7XdlQ","authDomain":"p0-gcp-project.firebaseapp.com","projectId":"p0-gcp-project","storageBucket":"p0-gcp-project.appspot.com","messagingSenderId":"398809717501","appId":"1:398809717501:web:6dd1cab893b2faeb06fc94"},"google":{"clientId":"398809717501-j4j8ldjicsspidl4ecj6nj1oomo9eda1.apps.googleusercontent.com","clientSecret":"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"},"appUrl":"http://localhost:8088","environment":"production"}",
{
"mode": "600",
},
],
[
"/path/to/home/.p0",
"{
Expand Down
2 changes: 1 addition & 1 deletion src/commands/__tests__/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe("login", () => {
await login({ org: "test-org" });
expect(pluginLoginMap.google).toHaveBeenCalled();
});
it("should write the user's identity to the file system", async () => {
it("should write the user's identity & config to the file system", async () => {
await login({ org: "test-org" });
expect(mockWriteFile.mock.calls).toMatchSnapshot();
});
Expand Down
4 changes: 2 additions & 2 deletions src/commands/allow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You should have received a copy of the GNU General Public License along with @p0
**/
import { fetchCommand } from "../drivers/api";
import { authenticate } from "../drivers/auth";
import { guard } from "../drivers/firestore";
import { fsShutdownGuard } from "../drivers/firestore";
import { print2 } from "../drivers/stdio";
import { AllowResponse } from "../types/allow";
import { Authn } from "../types/identity";
Expand All @@ -37,7 +37,7 @@ export const allowCommand = (yargs: yargs.Argv) =>
"allow [arguments..]",
"Create standing access for a resource",
allowArgs,
guard(allow)
fsShutdownGuard(allow)
);

export const allow = async (
Expand Down
6 changes: 3 additions & 3 deletions src/commands/aws/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You should have received a copy of the GNU General Public License along with @p0
**/
import { parseXml } from "../../common/xml";
import { authenticate } from "../../drivers/auth";
import { guard } from "../../drivers/firestore";
import { fsShutdownGuard } from "../../drivers/firestore";
import { print1, print2 } from "../../drivers/stdio";
import { getAwsConfig } from "../../plugins/aws/config";
import { AwsFederatedLogin, AwsItem } from "../../plugins/aws/types";
Expand All @@ -29,7 +29,7 @@ export const role = (yargs: yargs.Argv<{ account: string | undefined }>) =>
"List available AWS roles",
identity,
// TODO: select based on uidLocation
guard(oktaAwsListRoles)
fsShutdownGuard(oktaAwsListRoles)
)
.command(
"assume <role>",
Expand All @@ -41,7 +41,7 @@ export const role = (yargs: yargs.Argv<{ account: string | undefined }>) =>
describe: "An AWS role name",
}),
// TODO: select based on uidLocation
guard(oktaAwsAssumeRole)
fsShutdownGuard(oktaAwsAssumeRole)
)
.demandCommand(1)
);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file is part of @p0security/cli

You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { guard } from "../drivers/firestore";
import { fsShutdownGuard } from "../drivers/firestore";
import { request, requestArgs } from "./shared/request";
import yargs from "yargs";

Expand All @@ -17,5 +17,5 @@ export const grantCommand = (yargs: yargs.Argv) =>
"grant [arguments..]",
"Grant access to another identity",
requestArgs,
guard(request("grant"))
fsShutdownGuard(request("grant"))
);
4 changes: 2 additions & 2 deletions src/commands/kubeconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ You should have received a copy of the GNU General Public License along with @p0
import { retryWithSleep } from "../common/retry";
import { AnsiSgr } from "../drivers/ansi";
import { authenticate } from "../drivers/auth";
import { guard } from "../drivers/firestore";
import { fsShutdownGuard } from "../drivers/firestore";
import { print2, spinUntil } from "../drivers/stdio";
import { parseArn } from "../plugins/aws/utils";
import {
Expand Down Expand Up @@ -65,7 +65,7 @@ export const kubeconfigCommand = (yargs: yargs.Argv) =>
describe:
"Requested duration for access (format like '10 minutes', '2 hours', '5 days', or '1 week')",
}),
guard(kubeconfigAction)
fsShutdownGuard(kubeconfigAction)
);

const kubeconfigAction = async (
Expand Down
23 changes: 18 additions & 5 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ You should have received a copy of the GNU General Public License along with @p0
import {
IDENTITY_CACHE_PATH,
IDENTITY_FILE_PATH,
authenticate,
loadCredentials,
} from "../drivers/auth";
import { doc, guard } from "../drivers/firestore";
import { saveConfig } from "../drivers/config";
import { bootstrapConfig } from "../drivers/env";
import {
authenticateToFirebase,
fsShutdownGuard,
publicDoc,
} from "../drivers/firestore";
import { print2 } from "../drivers/stdio";
import { pluginLoginMap } from "../plugins/login";
import { TokenResponse } from "../types/oidc";
Expand All @@ -32,10 +38,14 @@ export const login = async (
args: { org: string },
options?: { skipAuthenticate?: boolean }
) => {
const orgDoc = await getDoc<RawOrgData, object>(doc(`orgs/${args.org}`));
const orgDoc = await getDoc<RawOrgData, object>(
publicDoc(`orgs/${args.org}`)
);
const orgData = orgDoc.data();
if (!orgData) throw "Could not find organization";

await saveConfig(orgData.config ?? bootstrapConfig);

const orgWithSlug: OrgData = { ...orgData, slug: args.org };

const plugin = orgWithSlug?.ssoProvider;
Expand All @@ -49,7 +59,10 @@ export const login = async (
await writeIdentity(orgWithSlug, tokenResponse);

// validate auth
if (!options?.skipAuthenticate) await authenticate({ noRefresh: true });
if (!options?.skipAuthenticate) {
const identity = await loadCredentials({ noRefresh: true });
await authenticateToFirebase(identity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It used to call authenticate, so that function was reused in the login and all the other commands. I tried to understand why we can't reuse anymore. Can you explain please?

Copy link
Contributor Author

@fabgo fabgo Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because authenticate now saves the config & initializes firebase. I would like to rename the authenticate method now, but couldn't think of a better name.

And there used to be a circular call chain, which I am trying to break:

authenticate -> loadCredentials -> login -> authenticate

}

print2(`You are now logged in, and can use the p0 CLI.`);
};
Expand Down Expand Up @@ -95,5 +108,5 @@ export const loginCommand = (yargs: yargs.Argv) =>
type: "string",
describe: "Your P0 organization ID",
}),
guard(login)
fsShutdownGuard(login)
);
4 changes: 2 additions & 2 deletions src/commands/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ You should have received a copy of the GNU General Public License along with @p0
import { AnsiSgr } from "../drivers/ansi";
import { fetchCommand } from "../drivers/api";
import { authenticate } from "../drivers/auth";
import { guard } from "../drivers/firestore";
import { fsShutdownGuard } from "../drivers/firestore";
import { print2, print1, spinUntil } from "../drivers/stdio";
import { max, orderBy } from "lodash";
import pluralize from "pluralize";
Expand Down Expand Up @@ -45,7 +45,7 @@ export const lsCommand = (yargs: yargs.Argv) =>
"ls [arguments..]",
"List request-command arguments",
lsArgs,
guard(ls)
fsShutdownGuard(ls)
);

const ls = async (
Expand Down
4 changes: 2 additions & 2 deletions src/commands/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file is part of @p0security/cli

You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { guard } from "../drivers/firestore";
import { fsShutdownGuard } from "../drivers/firestore";
import { request, requestArgs } from "./shared/request";
import yargs from "yargs";

Expand All @@ -17,5 +17,5 @@ export const requestCommand = (yargs: yargs.Argv) =>
"request [arguments..]",
"Manually request permissions on a resource",
requestArgs,
guard(request("request"))
fsShutdownGuard(request("request"))
);
4 changes: 2 additions & 2 deletions src/commands/scp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This file is part of @p0security/cli
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { authenticate } from "../drivers/auth";
import { guard } from "../drivers/firestore";
import { fsShutdownGuard } from "../drivers/firestore";
import { sshOrScp } from "../plugins/ssh";
import { SshRequest, SupportedSshProviders } from "../types/ssh";
import { prepareRequest, ScpCommandArgs } from "./shared/ssh";
Expand Down Expand Up @@ -69,7 +69,7 @@ export const scpCommand = (yargs: yargs.Argv) =>
The '--' argument must be specified between P0-specific args on the left and SCP_ARGS on the right.`
),

guard(scpAction)
fsShutdownGuard(scpAction)
);

/** Transfers files between a local and remote hosts using SSH.
Expand Down
4 changes: 2 additions & 2 deletions src/commands/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This file is part of @p0security/cli
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { authenticate } from "../drivers/auth";
import { guard } from "../drivers/firestore";
import { fsShutdownGuard } from "../drivers/firestore";
import { sshOrScp } from "../plugins/ssh";
import { SshCommandArgs, prepareRequest } from "./shared/ssh";
import yargs from "yargs";
Expand Down Expand Up @@ -69,7 +69,7 @@ export const sshCommand = (yargs: yargs.Argv) =>
$ p0 ssh example-instance --provider gcloud -- -NR '*:8080:localhost:8088' -o 'GatewayPorts yes'`
),

guard(sshAction)
fsShutdownGuard(sshAction)
);

/** Connect to an SSH backend
Expand Down
5 changes: 2 additions & 3 deletions src/drivers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ This file is part of @p0security/cli

You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { config } from "../drivers/env";
import { Authn } from "../types/identity";
import { getTenantConfig } from "./config";
import * as path from "node:path";
import yargs from "yargs";

const tenantUrl = (tenant: string) => `${config.appUrl}/o/${tenant}`;

const tenantUrl = (tenant: string) => `${getTenantConfig().appUrl}/o/${tenant}`;
const commandUrl = (tenant: string) => `${tenantUrl(tenant)}/command/`;

export const fetchCommand = async <T>(
Expand Down
29 changes: 6 additions & 23 deletions src/drivers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ You should have received a copy of the GNU General Public License along with @p0
import { login } from "../commands/login";
import { Authn, Identity } from "../types/identity";
import { P0_PATH } from "../util";
import { auth } from "./firestore";
import { loadConfig } from "./config";
import { authenticateToFirebase, initializeFirebase } from "./firestore";
import { print2 } from "./stdio";
import {
OAuthProvider,
SignInMethod,
signInWithCredential,
} from "firebase/auth";
import * as fs from "fs/promises";
import * as path from "path";

Expand Down Expand Up @@ -96,24 +92,11 @@ export const loadCredentials = async (options?: {
export const authenticate = async (options?: {
noRefresh?: boolean;
}): Promise<Authn> => {
const identity = await loadCredentials(options);
const { credential } = identity;
await loadConfig();
initializeFirebase();

// TODO: Move to map lookup
const provider = new OAuthProvider(
identity.org.ssoProvider === "google"
? SignInMethod.GOOGLE
: identity.org.providerId
);
const firebaseCredential = provider.credential({
accessToken: credential.access_token,
idToken: credential.id_token,
});
auth.tenantId = identity.org.tenantId;
const userCredential = await signInWithCredential(auth, firebaseCredential);
if (!userCredential?.user?.email) {
throw "Can not sign in: this user has previously signed in with a different identity provider.\nPlease contact support@p0.dev to enable this user.";
}
const identity = await loadCredentials(options);
const userCredential = await authenticateToFirebase(identity);

return { userCredential, identity };
};
35 changes: 35 additions & 0 deletions src/drivers/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** Copyright © 2024-present P0 Security

This file is part of @p0security/cli

@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.

@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.

You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { Config } from "../types/org";
import { P0_PATH } from "../util";
import { print2 } from "./stdio";
import fs from "fs/promises";
import path from "path";

export const CONFIG_FILE_PATH = path.join(P0_PATH, "config.json");

let tenantConfig: Config;

export function getTenantConfig(): Config {
return tenantConfig;
}

export async function saveConfig(config: Config) {
print2(`Saving config to ${CONFIG_FILE_PATH}.`);
const dir = path.dirname(CONFIG_FILE_PATH);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(CONFIG_FILE_PATH, JSON.stringify(config), { mode: "600" });
}

export async function loadConfig() {
const buffer = await fs.readFile(CONFIG_FILE_PATH);
tenantConfig = JSON.parse(buffer.toString());
}
2 changes: 1 addition & 1 deletion src/drivers/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dotenv.config();

const { env } = process;

export const config = {
export const bootstrapConfig = {
fs: {
// Falls back to public production Firestore credentials
apiKey: env.P0_FS_API_KEY ?? "AIzaSyCaL-Ik_l_5tdmgNUNZ4Nv6NuR4o5_PPfs",
Expand Down
Loading
Loading