Skip to content

Commit

Permalink
feat: emui auth via cookie (#1783)
Browse files Browse the repository at this point in the history
## Description:
This PR modifies how the AuthenticatedKurtosisClient is created by
retrieving authentication material from a cookie, rather than via a
passed message.

This PR adds back the connect button.

Finally, this pr adds support for locally running proxied inside of the
cloud frontend behind a local nginx server. This improves the
development experience of the cloud/emui portal.

## Is this change user facing?
YES - This is also a breaking change and will require the corresponding
change in the cloud frontend to be deployed to communicate the new auth
material properly

---------

Co-authored-by: Anders Schwartz <adschwartz@users.noreply.github.com>
Co-authored-by: Anders Schwartz <anders.schwartz@kurtosistech.com>
  • Loading branch information
3 people committed Nov 20, 2023
1 parent b558d38 commit d5d79d8
Show file tree
Hide file tree
Showing 23 changed files with 102 additions and 115 deletions.
5 changes: 4 additions & 1 deletion .circleci/config.yml
Expand Up @@ -1167,7 +1167,10 @@ workflows:
"pattern": "https://twitter.com/.*"
},
{
"pattern": "http://localhost:3000"
"pattern": "http://localhost:.*"
},
{
"pattern": "http://localhost/.*"
}
]
}
Expand Down
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
2 changes: 2 additions & 0 deletions enclave-manager/web/package.json
Expand Up @@ -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,6 +32,7 @@
"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",
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 @@ -62,13 +61,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;
}
}
14 changes: 0 additions & 14 deletions enclave-manager/web/src/components/LocationBroadcaster.tsx

This file was deleted.

17 changes: 0 additions & 17 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
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 @@ -62,11 +60,9 @@ const KurtosisRouter = () => {
[
{
element: (
<AppLayout Nav={<Navbar baseApplicationUrl={kurtosisClient.getBaseApplicationUrl()} />}>
<AppLayout Nav={<Navbar />}>
<Outlet />
<CreateEnclave />
<LocationBroadcaster />
<LocationListener />
</AppLayout>
),
children: [
Expand Down
23 changes: 11 additions & 12 deletions enclave-manager/web/src/emui/Navbar.tsx
@@ -1,29 +1,28 @@
import { FiHome } from "react-icons/fi";
import { PiLinkSimpleBold } from "react-icons/pi";
import { Link, useLocation } from "react-router-dom";
import { KURTOSIS_CLOUD_CONNECT_URL } from "../client/constants";
import { useKurtosisClient } from "../client/enclaveManager/KurtosisClientContext";
import { NavButton, Navigation } from "../components/Navigation";

export type NavbarProps = {
baseApplicationUrl: URL;
};

export const Navbar = ({ baseApplicationUrl }: NavbarProps) => {
export const Navbar = () => {
const location = useLocation();
// const kurtosisClient = useKurtosisClient();
const kurtosisClient = useKurtosisClient();

return (
<Navigation baseApplicationUrl={baseApplicationUrl}>
<Navigation>
<Link to={"/"}>
<NavButton
label={"View enclaves"}
Icon={<FiHome />}
isActive={location.pathname === "/" || location.pathname.startsWith("/enclave")}
/>
</Link>
{/*{kurtosisClient.isRunningInCloud() && (*/}
{/* <Link to={KURTOSIS_CLOUD_CONNECT_URL}>*/}
{/* <NavButton label={"Link your CLI"} Icon={<PiLinkSimpleBold />} isActive={true} />*/}
{/* </Link>*/}
{/*)}*/}
{kurtosisClient.isRunningInCloud() && (
<Link to={KURTOSIS_CLOUD_CONNECT_URL}>
<NavButton label={"Link your CLI"} Icon={<PiLinkSimpleBold />} />
</Link>
)}
{/*<Link to={"/catalog"}>*/}
{/* <NavButton label={"View catalog"} Icon={<FiPackage />} isActive={location.pathname.startsWith("/catalog")} />*/}
{/*</Link>*/}
Expand Down

0 comments on commit d5d79d8

Please sign in to comment.