diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 1c49141a0..5cd540862 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -11,7 +11,7 @@ import { getFeatureEndpointOrDefault, queryDocuments, readDocument, - updateDocument, + updateDocument } from "./MongoProxyClient"; const databaseId = "testDB"; @@ -236,13 +236,12 @@ describe("MongoProxyClient", () => { }); it("returns a production endpoint", () => { - const endpoint = getEndpoint(); + const endpoint = getEndpoint("https://main.documentdb.ext.azure.com"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); }); it("returns a development endpoint", () => { - updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); - const endpoint = getEndpoint(); + const endpoint = getEndpoint("https://localhost:1234"); expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer"); }); @@ -250,7 +249,7 @@ describe("MongoProxyClient", () => { updateUserContext({ authType: AuthType.EncryptedToken, }); - const endpoint = getEndpoint(); + const endpoint = getEndpoint("https://main.documentdb.ext.azure.com"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); }); }); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 668a0ab16..22feaab85 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -1,7 +1,8 @@ import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import queryString from "querystring"; +import { validateEndpoint } from "Utils/EndpointValidation"; import { AuthType } from "../AuthType"; -import { configContext } from "../ConfigContext"; +import { allowedMongoProxyEndpoints, configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { Collection } from "../Contracts/ViewModels"; @@ -336,14 +337,16 @@ export function createMongoCollectionWithProxy( } export function getFeatureEndpointOrDefault(feature: string): string { - return hasFlag(userContext.features.mongoProxyAPIs, feature) - ? getEndpoint(userContext.features.mongoProxyEndpoint) - : getEndpoint(); + + const endpoint = (hasFlag(userContext.features.mongoProxyAPIs, feature) && validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)) + ? userContext.features.mongoProxyEndpoint + : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; + + return getEndpoint(endpoint); } -export function getEndpoint(customEndpoint?: string): string { - let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; - url += "/api/mongo/explorer"; +export function getEndpoint(endpoint: string): string { + let url = endpoint + "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { url = url.replace("api/mongo", "api/guest/mongo"); diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 289e650cb..f14e0223b 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,3 +1,5 @@ +import { allowedAadEndpoints, allowedArcadiaEndpoints, allowedArcadiaLivyDnsZones, allowedArmEndpoints, allowedBackendEndpoints, allowedEmulatorEndpoints, allowedGraphEndpoints, allowedHostedExplorerEndpoints, allowedJunoEndpoints, allowedMongoBackendEndpoints, allowedMsalRedirectEndpoints, validateEndpoint } from "Utils/EndpointValidation"; + export enum Platform { Portal = "Portal", Hosted = "Hosted", @@ -6,7 +8,6 @@ export enum Platform { export interface ConfigContext { platform: Platform; - allowedParentFrameOrigins: string[]; gitSha?: string; proxyPath?: string; AAD_ENDPOINT: string; @@ -26,21 +27,12 @@ export interface ConfigContext { GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. hostedExplorerURL: string; armAPIVersion?: string; - allowedJunoOrigins: string[]; msalRedirectURI?: string; } // Default configuration let configContext: Readonly = { platform: Platform.Portal, - allowedParentFrameOrigins: [ - `^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`, - `^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`, - `^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`, - `^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`, - `^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`, - `^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`, - ], // Webpack injects this at build time gitSha: process.env.GIT_SHA, hostedExplorerURL: "https://cosmos.azure.com/", @@ -55,13 +47,6 @@ let configContext: Readonly = { GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306 JUNO_ENDPOINT: "https://tools.cosmos.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", - allowedJunoOrigins: [ - "https://juno-test.documents-dev.windows-int.net", - "https://juno-test2.documents-dev.windows-int.net", - "https://tools.cosmos.azure.com", - "https://tools-staging.cosmos.azure.com", - "https://localhost", - ], }; export function resetConfigContext(): void { @@ -72,6 +57,54 @@ export function resetConfigContext(): void { } export function updateConfigContext(newContext: Partial): void { + if (!newContext) { + return; + } + + if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) { + delete newContext.ARM_ENDPOINT; + } + + if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) { + delete newContext.AAD_ENDPOINT; + } + + if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) { + delete newContext.EMULATOR_ENDPOINT; + } + + if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) { + delete newContext.GRAPH_ENDPOINT; + } + + if (!validateEndpoint(newContext.ARCADIA_ENDPOINT, allowedArcadiaEndpoints)) { + delete newContext.ARCADIA_ENDPOINT; + } + + if (!validateEndpoint(newContext.ARCADIA_LIVY_ENDPOINT_DNS_ZONE, allowedArcadiaLivyDnsZones)) { + delete newContext.ARCADIA_LIVY_ENDPOINT_DNS_ZONE; + } + + if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) { + delete newContext.BACKEND_ENDPOINT; + } + + if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) { + delete newContext.MONGO_BACKEND_ENDPOINT; + } + + if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoEndpoints)) { + delete newContext.JUNO_ENDPOINT; + } + + if (!validateEndpoint(newContext.hostedExplorerURL, allowedHostedExplorerEndpoints)) { + delete newContext.hostedExplorerURL; + } + + if (!validateEndpoint(newContext.msalRedirectURI, allowedMsalRedirectEndpoints)) { + delete newContext.msalRedirectURI; + } + Object.assign(configContext, newContext); } @@ -112,20 +145,28 @@ export async function initializeConfiguration(): Promise { console.error(error); } } + // Allow override of platform value with URL query parameter const params = new URLSearchParams(window.location.search); if (params.has("armAPIVersion")) { const armAPIVersion = params.get("armAPIVersion") || ""; updateConfigContext({ armAPIVersion }); } + if (params.has("armEndpoint")) { const ARM_ENDPOINT = params.get("armEndpoint") || ""; - updateConfigContext({ ARM_ENDPOINT }); + if (validateEndpoint(ARM_ENDPOINT, configContext.validArmEndpoints)) { + updateConfigContext({ ARM_ENDPOINT }); + } } + if (params.has("aadEndpoint")) { const AAD_ENDPOINT = params.get("aadEndpoint") || ""; - updateConfigContext({ AAD_ENDPOINT }); + if (validateEndpoint(AAD_ENDPOINT, configContext.validAadEndpoints)) { + updateConfigContext({ AAD_ENDPOINT }); + } } + if (params.has("platform")) { const platform = params.get("platform"); switch (platform) { @@ -145,3 +186,4 @@ export async function initializeConfiguration(): Promise { } export { configContext }; + diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 6a3a49bfb..6eb8ed626 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,8 +1,10 @@ import { Link } from "@fluentui/react/lib/Link"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; +import { allowedNotebookServerUrls } from "ConfigContext"; import * as ko from "knockout"; import React from "react"; import _ from "underscore"; +import { validateEndpoint } from "Utils/EndpointValidation"; import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; @@ -31,7 +33,7 @@ import { get as getWorkspace, listByDatabaseAccount, listConnectionInfo, - start, + start } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; @@ -174,7 +176,7 @@ export default class Explorer { this.resourceTree = new ResourceTreeAdapter(this); // Override notebook server parameters from URL parameters - if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) { + if (userContext.features.notebookServerUrl && validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) && userContext.features.notebookServerToken) { useNotebook.getState().setNotebookServerInfo({ notebookServerEndpoint: userContext.features.notebookServerUrl, authToken: userContext.features.notebookServerToken, @@ -186,19 +188,6 @@ export default class Explorer { useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); } - if (userContext.features.livyEndpoint) { - useNotebook.getState().setSparkClusterConnectionInfo({ - userName: undefined, - password: undefined, - endpoints: [ - { - endpoint: userContext.features.livyEndpoint, - kind: DataModels.SparkClusterEndpointKind.Livy, - }, - ], - }); - } - this.refreshExplorer(); } @@ -362,7 +351,7 @@ export default class Explorer { ); useNotebook.getState().setNotebookServerInfo({ - notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, + notebookServerEndpoint: (validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) && userContext.features.notebookServerUrl) || connectionInfo.notebookServerEndpoint, authToken: userContext.features.notebookServerToken || connectionInfo.authToken, forwardingId: undefined, }); @@ -421,7 +410,7 @@ export default class Explorer { connectionStatus.status = ConnectionStatusType.Connected; useNotebook.getState().setConnectionInfo(connectionStatus); useNotebook.getState().setNotebookServerInfo({ - notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, + notebookServerEndpoint: validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) && userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken, forwardingId: connectionInfo.data.forwardingId, }); diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 1a43cccc6..e7513b6a6 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -1,6 +1,7 @@ import ko from "knockout"; +import { validateEndpoint } from "Utils/EndpointValidation"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; -import { configContext } from "../ConfigContext"; +import { allowedJunoOrigins, configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; import { IGitHubResponse } from "../GitHub/GitHubClient"; @@ -483,7 +484,7 @@ export class JunoClient { // public for tests public static getJunoEndpoint(): string { const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; - if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) { + if (validateEndpoint(junoEndpoint, allowedJunoOrigins)) { const error = `${junoEndpoint} not allowed as juno endpoint`; console.error(error); throw new Error(error); diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index a52e021bc..eca5adf5b 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -1,13 +1,14 @@ +import { validateEndpoint } from "Utils/EndpointValidation"; import { ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../Common/Constants"; import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; -import { configContext } from "../ConfigContext"; +import { allowedJunoOrigins, configContext } from "../ConfigContext"; import { ContainerInfo, IContainerData, IPhoenixConnectionInfoResult, IProvisionData, - IResponse, + IResponse } from "../Contracts/DataModels"; import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { userContext } from "../UserContext"; @@ -103,9 +104,8 @@ export class PhoenixClient { } public static getPhoenixEndpoint(): string { - const phoenixEndpoint = - userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; - if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) { + const phoenixEndpoint = userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; + if (validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) { const error = `${phoenixEndpoint} not allowed as juno endpoint`; console.error(error); throw new Error(error); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 655b9430a..0840fc598 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -22,7 +22,6 @@ export type Features = { readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly phoenixEndpoint?: string; - readonly livyEndpoint?: string; readonly notebookBasePath?: string; readonly notebookServerToken?: string; readonly notebookServerUrl?: string; @@ -72,7 +71,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear mongoProxyAPIs: get("mongoproxyapis"), junoEndpoint: get("junoendpoint"), phoenixEndpoint: get("phoenixendpoint"), - livyEndpoint: get("livyendpoint"), notebookBasePath: get("notebookbasepath"), notebookServerToken: get("notebookservertoken"), notebookServerUrl: get("notebookserverurl"), diff --git a/src/Utils/EndpointValidation.ts b/src/Utils/EndpointValidation.ts new file mode 100644 index 000000000..effbd845e --- /dev/null +++ b/src/Utils/EndpointValidation.ts @@ -0,0 +1,82 @@ +export function validateEndpoint(endpointToValidate: string, allowedEndpoints: ReadonlyArray): boolean { + if (!endpointToValidate) { + return true; + } + const originToValidate: string = new URL(endpointToValidate).origin; + const allowedOrigins: string[] = allowedEndpoints.map(allowedEndpoint => new URL(allowedEndpoint).origin) || []; + return allowedOrigins.indexOf(originToValidate) >= 0; +} + +export const allowedArmEndpoints: ReadonlyArray = [ + "https://​management.azure.com", + "https://​management.usgovcloudapi.net", + "https://management.chinacloudapi.cn" +]; + +export const allowedAadEndpoints: ReadonlyArray = [ + "https://login.microsoftonline.com/" +]; + +export const allowedParentFrameOrigins: ReadonlyArray = [ + `^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`, + `^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`, + `^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`, + `^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`, + `^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`, + `^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`, +]; + +export const allowedJunoOrigins: ReadonlyArray = [ + "https://juno-test.documents-dev.windows-int.net", + "https://juno-test2.documents-dev.windows-int.net", + "https://tools.cosmos.azure.com", + "https://tools-staging.cosmos.azure.com", + "https://localhost", +]; + +export const allowedEmulatorEndpoints: ReadonlyArray = [ +]; + +export const allowedGraphEndpoints: ReadonlyArray = [ + +]; + +export const allowedArcadiaEndpoints: ReadonlyArray = [ + +]; + +export const allowedArcadiaLivyDnsZones: ReadonlyArray = [ + +]; + +export const allowedBackendEndpoints: ReadonlyArray = [ + +]; + +export const allowedMongoBackendEndpoints: ReadonlyArray = [ + +]; + +export const allowedJunoEndpoints: ReadonlyArray = [ + +]; + +export const allowedHostedExplorerEndpoints: ReadonlyArray = [ + +]; + +export const allowedMsalRedirectEndpoints: ReadonlyArray = [ + +]; + +export const allowedMongoProxyEndpoints: ReadonlyArray = [ + +]; + +export const allowedPhoenixEndpoints: ReadonlyArray = [ + +]; + +export const allowedNotebookServerUrls: ReadonlyArray = [ + +];