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

feat: emui auth via cookie #1783

Merged
merged 14 commits into from Nov 20, 2023
5 changes: 5 additions & 0 deletions enclave-manager/web/.env.cloudDevelopment
@@ -0,0 +1,5 @@
REACT_APP_KURTOSIS_DEFAULT_HOST=localhost
REACT_APP_KURTOSIS_DEFAULT_EM_API_PORT=8081

REACT_APP_KURTOSIS_CLOUD_UI_URL=http://localhost:3000
REACT_APP_KURTOSIS_PACKAGE_INDEXER_URL=https://cloud.kurtosis.com:9770
6 changes: 5 additions & 1 deletion enclave-manager/web/README.md
Expand Up @@ -22,7 +22,7 @@ Removes the build output if present.
### `yarn start`

Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
Open [http://localhost:4000](http://localhost:4000) to view it in the browser.

The page will reload if you make edits.\
You will also see any lint errors in the console.
Expand All @@ -42,6 +42,10 @@ Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `yarn start:prod`

Serve your local build on port 4000.

### `yarn eject`

**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
Expand Down
11 changes: 8 additions & 3 deletions enclave-manager/web/package.json
Expand Up @@ -2,7 +2,7 @@
"name": "enclave-manager-ui",
"version": "0.1.0",
"private": true,
"homepage": ".",
"homepage": "./",
"dependencies": {
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.1",
Expand All @@ -17,6 +17,7 @@
"framer-motion": "^10.16.4",
"has-ansi": "^5.0.1",
"html-react-parser": "^4.2.2",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"luxon": "^3.4.3",
"react": "^18.2.0",
Expand All @@ -31,11 +32,13 @@
"true-myth": "^7.1.0"
},
"devDependencies": {
"@types/js-cookie": "^3.0.6",
"@types/luxon": "^3.3.3",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/streamsaver": "^2.0.4",
"dotenv-cli": "^6.0.0",
"monaco-editor": "^0.44.0",
"prettier": "3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
Expand All @@ -48,9 +51,11 @@
"prebuild": "rm -rf ../../engine/server/webapp",
"clean": "rm -rf build",
"cleanInstall": "rm -rf node_modules; yarn install",
"start": "react-scripts start",
"start:prod": "serve -s build",
"start": "PORT=4000 react-scripts start",
"start:cloud": "BROWSER=none PUBLIC_URL=http://localhost:3000/emui-dev PORT=4000 dotenv -e ./.env.cloudDevelopment -- react-scripts start",
"start:prod": "serve -p 4000 -s build",
"build": "react-scripts build",
"build:cloudDev": "dotenv -e ./.env.cloudDevelopment -- react-scripts build",
"postbuild": "cp -r build/ ../../engine/server/webapp",
"prettier": "prettier . --check",
"prettier:fix": "prettier . --write",
Expand Down
Binary file not shown.
4 changes: 3 additions & 1 deletion enclave-manager/web/src/client/constants.ts
Expand Up @@ -4,11 +4,13 @@ import { isDefined } from "../utils";
export const KURTOSIS_CLOUD_PROTOCOL = "https";
export const KURTOSIS_CLOUD_HOST = "cloud.kurtosis.com";
export const KURTOSIS_CLOUD_CONNECT_PAGE = "connect";
export const KURTOSIS_CLOUD_EM_PAGE = "enclave-manager";

// Cloud
export const KURTOSIS_CLOUD_UI_URL =
process.env.REACT_APP_KURTOSIS_CLOUD_UI_URL || `${KURTOSIS_CLOUD_PROTOCOL}://${KURTOSIS_CLOUD_HOST}`;
export const KURTOSIS_CLOUD_CONNECT_URL = `${KURTOSIS_CLOUD_PROTOCOL}://${KURTOSIS_CLOUD_HOST}/${KURTOSIS_CLOUD_CONNECT_PAGE}`;
export const KURTOSIS_CLOUD_CONNECT_URL = `${KURTOSIS_CLOUD_UI_URL}/${KURTOSIS_CLOUD_CONNECT_PAGE}`;
export const KURTOSIS_CLOUD_EM_URL = `${KURTOSIS_CLOUD_UI_URL}/${KURTOSIS_CLOUD_EM_PAGE}`;
export const KURTOSIS_PACKAGE_INDEXER_URL =
process.env.REACT_APP_KURTOSIS_PACKAGE_INDEXER_URL || `${KURTOSIS_CLOUD_PROTOCOL}://${KURTOSIS_CLOUD_HOST}:9770`;

Expand Down
@@ -1,7 +1,8 @@
import { createPromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { KurtosisEnclaveManagerServer } from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_connect";
import { KURTOSIS_CLOUD_UI_URL, KURTOSIS_DEFAULT_EM_API_PORT } from "../constants";
import { DateTime } from "luxon";
import { KURTOSIS_CLOUD_EM_URL, KURTOSIS_CLOUD_UI_URL, KURTOSIS_DEFAULT_EM_API_PORT } from "../constants";
import { KurtosisClient } from "./KurtosisClient";

function constructGatewayURL(remoteHost: string): string {
Expand All @@ -10,6 +11,7 @@ function constructGatewayURL(remoteHost: string): string {

export class AuthenticatedKurtosisClient extends KurtosisClient {
private readonly token: string;
private readonly tokenExpiry: DateTime;

constructor(gatewayHost: string, token: string, parentUrl: URL, childUrl: URL) {
super(
Expand All @@ -21,11 +23,24 @@ export class AuthenticatedKurtosisClient extends KurtosisClient {
childUrl,
);
this.token = token;
const parsedToken = JSON.parse(atob(this.token.split(".")[1]));
this.tokenExpiry = DateTime.fromSeconds(parsedToken["exp"]);
}

validateTokenStillFresh() {
if (this.tokenExpiry < DateTime.now()) {
console.log("Token has expired. Triggering a refresh");
window.location.href = KURTOSIS_CLOUD_EM_URL;
}
}

getHeaderOptions(): { headers?: Headers } {
this.validateTokenStillFresh();
const headers = new Headers();
headers.set("Authorization", `Bearer ${this.token}`);
return { headers: headers };
}
isRunningInCloud(): boolean {
return true;
}
}
Expand Up @@ -26,7 +26,6 @@ import {
import { EnclaveFullInfo } from "../../emui/enclaves/types";
import { assertDefined, asyncResult, isDefined } from "../../utils";
import { RemoveFunctions } from "../../utils/types";
import { KURTOSIS_CLOUD_HOST } from "../constants";

export abstract class KurtosisClient {
protected readonly client: PromiseClient<typeof KurtosisEnclaveManagerServer>;
Expand Down Expand Up @@ -65,13 +64,7 @@ export abstract class KurtosisClient {
return undefined;
}

getCloudUrl() {
return this.cloudUrl;
}

isRunningInCloud() {
return this.cloudUrl.host.toLowerCase().includes(KURTOSIS_CLOUD_HOST);
}
abstract isRunningInCloud(): boolean;

abstract getHeaderOptions(): { headers?: Headers };

Expand Down
@@ -1,7 +1,9 @@
import { Flex, Heading, Spinner } from "@chakra-ui/react";
import Cookies from "js-cookie";
import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from "react";
import { KurtosisAlert } from "../../components/KurtosisAlert";
import { assertDefined, isDefined, isStringTrue, stringifyError } from "../../utils";
import { assertDefined, isDefined, stringifyError } from "../../utils";
import { KURTOSIS_CLOUD_EM_PAGE, KURTOSIS_CLOUD_EM_URL } from "../constants";
import { AuthenticatedKurtosisClient } from "./AuthenticatedKurtosisClient";
import { KurtosisClient } from "./KurtosisClient";
import { LocalKurtosisClient } from "./LocalKurtosisClient";
Expand All @@ -14,7 +16,6 @@ const KurtosisClientContext = createContext<KurtosisClientContextState>({ client

export const KurtosisClientProvider = ({ children }: PropsWithChildren) => {
const [client, setClient] = useState<KurtosisClient>();
const [jwtToken, setJwtToken] = useState<string>();
const [error, setError] = useState<string>();

const errorHandlingClient = useMemo(() => {
Expand Down Expand Up @@ -47,47 +48,39 @@ export const KurtosisClientProvider = ({ children }: PropsWithChildren) => {
return undefined;
}, [client]);

useEffect(() => {
const receiveMessage = (event: MessageEvent) => {
const message = event.data.message;
switch (message) {
case "jwtToken":
const value = event.data.value;
if (isDefined(value)) {
setJwtToken(value);
}
break;
}
};
window.addEventListener("message", receiveMessage);
return () => window.removeEventListener("message", receiveMessage);
}, []);

useEffect(() => {
(async () => {
const searchParams = new URLSearchParams(window.location.search);
const requireAuth = isStringTrue(searchParams.get("require-authentication"));
// If the pathname starts with /gateway` then we are trying to use an Authenticated client.
const path = window.location.pathname;

try {
setError(undefined);
let newClient: KurtosisClient | null = null;

if (requireAuth) {
const requestedGatewayHost = searchParams.get("api-host");
assertDefined(requestedGatewayHost, `The parameter 'api-host' is not defined`);
if (path.startsWith("/gateway")) {
const pathConfigPattern = /\/gateway\/ips\/([^/]+)\/ports\/([^/]+)(\/|$)/;
const matches = path.match(pathConfigPattern);
if (!matches) {
throw Error(`Cannot configure an authenticated kurtosis client on this path: \`${path}\``);
}

const gatewayHost = matches[1];
const port = parseInt(matches[2]);
if (isNaN(port)) {
throw Error(`Port ${port} is not a number.`);
}

// Get the parent location and path:
let parentLocationPath = paramToUrl(searchParams, "parent-location-path") || new URL(window.location.href);
// Get the child location and path:
let childLocationPath = paramToUrl(searchParams, "child-location-path") || new URL(window.location.href);
const jwtToken = Cookies.get("kurtosis");

if (isDefined(jwtToken)) {
newClient = new AuthenticatedKurtosisClient(
requestedGatewayHost,
`${gatewayHost}`,
jwtToken,
parentLocationPath,
childLocationPath,
new URL(`${window.location.protocol}//${window.location.host}/${KURTOSIS_CLOUD_EM_PAGE}`),
new URL(`${window.location.protocol}//${window.location.host}${matches[0]}`),
);
} else {
window.location.href = KURTOSIS_CLOUD_EM_URL;
}
} else {
newClient = new LocalKurtosisClient();
Expand All @@ -106,7 +99,7 @@ export const KurtosisClientProvider = ({ children }: PropsWithChildren) => {
setError(stringifyError(e));
}
})();
}, [jwtToken]);
}, []);

if (errorHandlingClient) {
return (
Expand Down Expand Up @@ -138,14 +131,3 @@ export const useKurtosisClient = (): KurtosisClient => {

return client;
};

const paramToUrl = (searchParams: URLSearchParams, param: string) => {
let paramString = searchParams.get(param);
if (paramString === null) {
return null;
} else {
paramString = atob(paramString);
assertDefined(paramString, `The parameter ${param}' is not defined`);
return new URL(paramString);
}
};
Expand Up @@ -20,4 +20,8 @@ export class LocalKurtosisClient extends KurtosisClient {
getHeaderOptions() {
return {};
}

isRunningInCloud(): boolean {
return false;
}
}
15 changes: 0 additions & 15 deletions enclave-manager/web/src/components/LocationBroadcaster.tsx

This file was deleted.

18 changes: 0 additions & 18 deletions enclave-manager/web/src/components/LocationListener.tsx

This file was deleted.

11 changes: 6 additions & 5 deletions enclave-manager/web/src/components/Navigation.tsx
@@ -1,11 +1,12 @@
import { Flex, IconButton, IconButtonProps, Image, Tooltip } from "@chakra-ui/react";
import { PropsWithChildren } from "react";
import { useKurtosisClient } from "../client/enclaveManager/KurtosisClientContext";

export type NavigationProps = {
baseApplicationUrl: URL;
};
export type NavigationProps = {};

export const Navigation = ({ children }: PropsWithChildren & NavigationProps) => {
const kurtosisClient = useKurtosisClient();

export const Navigation = ({ baseApplicationUrl, children }: PropsWithChildren & NavigationProps) => {
return (
<Flex
as={"nav"}
Expand All @@ -19,7 +20,7 @@ export const Navigation = ({ baseApplicationUrl, children }: PropsWithChildren &
p={"20px 16px"}
>
<Flex width={"40px"} height={"40px"} alignItems={"center"}>
<Image src={baseApplicationUrl + "/logo.png"} />
<Image src={kurtosisClient.getBaseApplicationUrl() + "/logo.png"} />
</Flex>
<Flex flexDirection={"column"} gap={"16px"}>
{children}
Expand Down
Expand Up @@ -242,7 +242,7 @@ export const ConfigureEnclaveModal = ({
<CopyButton contentName={"url"} valueToCopy={getLinkToCurrentConfig} text={"Copy link"} />
</Tooltip>
</Flex>
<KurtosisArgumentFormControl name={"enclaveName"} label={"Enclave name"} type={"string"}>
<KurtosisArgumentFormControl name={"enclaveName"} label={"Enclave name"} type={"text"}>
<StringArgumentInput
name={"enclaveName"}
disabled={isDefined(existingEnclave)}
Expand Down
6 changes: 1 addition & 5 deletions enclave-manager/web/src/emui/App.tsx
Expand Up @@ -8,8 +8,6 @@ import {
import { AppLayout } from "../components/AppLayout";
import { CreateEnclave } from "../components/enclaves/CreateEnclave";
import { KurtosisThemeProvider } from "../components/KurtosisThemeProvider";
import { LocationBroadcaster } from "../components/LocationBroadcaster";
import { LocationListener } from "../components/LocationListener";
import { catalogRoutes } from "./catalog/CatalogRoutes";
import { EmuiAppContextProvider } from "./EmuiAppContext";
import { enclaveRoutes } from "./enclaves/EnclaveRoutes";
Expand Down Expand Up @@ -39,11 +37,9 @@ const KurtosisRouter = () => {
[
{
element: (
<AppLayout Nav={<Navbar baseApplicationUrl={kurtosisClient.getBaseApplicationUrl()} />}>
<AppLayout Nav={<Navbar />}>
<Outlet />
<CreateEnclave />
<LocationBroadcaster />
<LocationListener />
</AppLayout>
),
children: [
Expand Down