Compare commits

..

2 Commits

Author SHA1 Message Date
Karthik Chakravarthy Cheekuri
272e88f351 Testing e2e coverage issue
wq
qa
2022-01-26 11:43:30 -05:00
Armando Trejo Oliver
f5da8bb276 Validate endpoints from feature flags (#1196)
Validate endpoints from feature flags
2022-01-24 13:06:43 -08:00
14 changed files with 220 additions and 29736 deletions

6
.vscode/launch.json vendored
View File

@@ -12,7 +12,8 @@
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand",
"--coverage", "false"
"--coverage",
"false"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
@@ -26,7 +27,8 @@
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"${fileBasenameNoExtension}",
"--coverage", "false",
"--coverage",
"false",
// "--watch",
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
// // https://github.com/facebook/jest/issues/6683

View File

@@ -2885,10 +2885,6 @@ a:link {
.settingsSectionPart {
padding-left: 8px;
label {
padding: 0 !important;
font-size: inherit;
}
}
.settingsSectionLabel {

29666
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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");
});
});

View File

@@ -1,5 +1,6 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import queryString from "querystring";
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -336,14 +337,17 @@ 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");

View File

@@ -1,4 +1,16 @@
import { JunoEndpoints } from "Common/Constants";
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedArmEndpoints,
allowedBackendEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints,
validateEndpoint,
} from "Utils/EndpointValidation";
export enum Platform {
Portal = "Portal",
@@ -8,7 +20,7 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedParentFrameOrigins: string[];
allowedParentFrameOrigins: ReadonlyArray<string>;
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
@@ -30,7 +42,6 @@ export interface ConfigContext {
isTerminalEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
allowedJunoOrigins: string[];
msalRedirectURI?: string;
}
@@ -44,8 +55,7 @@ let configContext: Readonly<ConfigContext> = {
`^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
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
@@ -61,14 +71,6 @@ let configContext: Readonly<ConfigContext> = {
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
isTerminalEnabled: false,
allowedJunoOrigins: [
JunoEndpoints.Test,
JunoEndpoints.Test2,
JunoEndpoints.Test3,
JunoEndpoints.Prod,
JunoEndpoints.Stage,
"https://localhost",
],
};
export function resetConfigContext(): void {
@@ -79,6 +81,50 @@ export function resetConfigContext(): void {
}
export function updateConfigContext(newContext: Partial<ConfigContext>): 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.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
delete newContext.BACKEND_ENDPOINT;
}
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
delete newContext.MONGO_BACKEND_ENDPOINT;
}
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
delete newContext.JUNO_ENDPOINT;
}
if (!validateEndpoint(newContext.hostedExplorerURL, allowedHostedExplorerEndpoints)) {
delete newContext.hostedExplorerURL;
}
if (!validateEndpoint(newContext.msalRedirectURI, allowedMsalRedirectEndpoints)) {
delete newContext.msalRedirectURI;
}
Object.assign(configContext, newContext);
}
@@ -102,18 +148,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
});
if (response.status === 200) {
try {
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
});
}
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
updateConfigContext({
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
});
}
const { ...externalConfig } = await response.json();
updateConfigContext(externalConfig);
} catch (error) {
console.error("Unable to parse json in config file");
console.error(error);

View File

@@ -1,8 +1,10 @@
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { IGalleryItem } from "Juno/JunoClient";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
@@ -24,7 +26,6 @@ import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs";
import { IGalleryItem } from "../Juno/JunoClient";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -50,7 +51,7 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
@@ -178,7 +179,11 @@ 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,
@@ -190,19 +195,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();
}
@@ -422,7 +414,10 @@ 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,
});
@@ -1283,7 +1278,6 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);

View File

@@ -188,11 +188,14 @@ export const SettingsPane: FunctionComponent = () => {
{shouldShowCrossPartitionOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<label className="settingsSectionLabel">Enable cross-partition query</label>
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</InfoTooltip>
<div className="settingsSectionLabel">
Enable cross-partition query
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</InfoTooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
@@ -208,14 +211,14 @@ export const SettingsPane: FunctionComponent = () => {
{shouldShowParallelismOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<label className="settingsSectionLabel" htmlFor="input65">
<div className="settingsSectionLabel">
Max degree of parallelism
</label>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</div>
<SpinButton
min={-1}

View File

@@ -103,14 +103,14 @@ exports[`Settings Pane should render Default properly 1`] = `
<div
className="settingsSectionPart"
>
<label
<div
className="settingsSectionLabel"
>
Enable cross-partition query
</label>
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</InfoTooltip>
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</InfoTooltip>
</div>
<StyledCheckboxBase
ariaLabel="Enable cross partition query"
checked={true}
@@ -132,15 +132,14 @@ exports[`Settings Pane should render Default properly 1`] = `
<div
className="settingsSectionPart"
>
<label
<div
className="settingsSectionLabel"
htmlFor="input65"
>
Max degree of parallelism
</label>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</div>
<StyledSpinButton
ariaLabel="Max degree of parallelism"
className="textfontclr"

View File

@@ -1,4 +1,5 @@
import ko from "knockout";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
@@ -484,7 +485,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);

View File

@@ -1,5 +1,6 @@
import promiseRetry, { AbortError } from "p-retry";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import {
Areas,
ConnectionStatusType,
@@ -154,7 +155,7 @@ 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) {
if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error);
throw new Error(error);

View File

@@ -23,7 +23,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"),

View File

@@ -0,0 +1,83 @@
import { JunoEndpoints } from "Common/Constants";
import * as Logger from "../Common/Logger";
export function validateEndpoint(
endpointToValidate: string | undefined,
allowedEndpoints: ReadonlyArray<string>
): boolean {
try {
return validateEndpointInternal(
endpointToValidate,
allowedEndpoints.map((e) => e)
);
} catch (reason) {
Logger.logError(`${endpointToValidate} not allowed`, "validateEndpoint");
Logger.logError(`${JSON.stringify(reason)}`, "validateEndpoint");
return false;
}
}
function validateEndpointInternal(
endpointToValidate: string | undefined,
allowedEndpoints: ReadonlyArray<string>
): boolean {
if (endpointToValidate === undefined) {
return false;
}
const originToValidate: string = new URL(endpointToValidate).origin;
const allowedOrigins: string[] = allowedEndpoints.map((allowedEndpoint) => new URL(allowedEndpoint).origin) || [];
const valid = allowedOrigins.indexOf(originToValidate) >= 0;
if (!valid) {
throw new Error(
`${endpointToValidate} is not an allowed endpoint. Allowed endpoints are ${allowedArmEndpoints.toString()}`
);
}
return valid;
}
export const allowedArmEndpoints: ReadonlyArray<string> = [
"https://management.azure.com",
"https://management.usgovcloudapi.net",
"https://management.chinacloudapi.cn",
];
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
export const allowedBackendEndpoints: ReadonlyArray<string> = [
"https://main.documentdb.ext.azure.com",
"https://localhost:12901",
"https://localhost:1234",
];
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
"https://main.documentdb.ext.azure.com",
"https://localhost:12901",
];
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.windows.net"];
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
"https://cosmos-explorer-preview.azurewebsites.net/",
];
export const allowedJunoOrigins: ReadonlyArray<string> = [
JunoEndpoints.Test,
JunoEndpoints.Test2,
JunoEndpoints.Test3,
JunoEndpoints.Prod,
JunoEndpoints.Stage,
"https://localhost",
];
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];

View File

@@ -4,7 +4,7 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
}
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) {