From 914e969083c5765924878ca217538c944908cd1e Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Mon, 19 Apr 2021 17:38:53 -0700 Subject: [PATCH] Add a feature flag to override Juno endpoint (#700) * Add a feature flag to override Juno endpoint * Fix build --- src/ConfigContext.ts | 15 ++++++++- src/Juno/JunoClient.test.ts | 43 +++++++++++++++++--------- src/Juno/JunoClient.ts | 17 ++++++++-- src/Platform/Hosted/extractFeatures.ts | 5 ++- 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 654928c6b..dc7d5f887 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -26,6 +26,7 @@ export interface ConfigContext { GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. hostedExplorerURL: string; armAPIVersion?: string; + allowedJunoOrigins: string[]; } // Default configuration @@ -53,6 +54,13 @@ 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 { @@ -86,13 +94,18 @@ export async function initializeConfiguration(): Promise { }); if (response.status === 200) { try { - const { allowedParentFrameOrigins, ...externalConfig } = await response.json(); + 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], + }); + } } catch (error) { console.error("Unable to parse json in config file"); console.error(error); diff --git a/src/Juno/JunoClient.test.ts b/src/Juno/JunoClient.test.ts index 8950e18f4..73e2a3cf8 100644 --- a/src/Juno/JunoClient.test.ts +++ b/src/Juno/JunoClient.test.ts @@ -1,10 +1,9 @@ import ko from "knockout"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; -import { IPinnedRepo, JunoClient, IPublishNotebookRequest } from "./JunoClient"; -import { configContext } from "../ConfigContext"; -import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { DatabaseAccount } from "../Contracts/DataModels"; import { updateUserContext, userContext } from "../UserContext"; +import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; +import { IPinnedRepo, IPublishNotebookRequest, JunoClient } from "./JunoClient"; const sampleSubscriptionId = "subscriptionId"; @@ -157,7 +156,7 @@ describe("Gallery", () => { const response = await junoClient.getSampleNotebooks(); expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/samples`, undefined); + expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/samples`, undefined); }); it("getPublicNotebooks", async () => { @@ -169,7 +168,7 @@ describe("Gallery", () => { const response = await junoClient.getPublicNotebooks(); expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/public`, undefined); + expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/public`, undefined); }); it("getNotebook", async () => { @@ -182,7 +181,7 @@ describe("Gallery", () => { const response = await junoClient.getNotebookInfo(id); expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`); + expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}`); }); it("getNotebookContent", async () => { @@ -195,7 +194,7 @@ describe("Gallery", () => { const response = await junoClient.getNotebookContent(id); expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/content`); + expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}/content`); }); it("increaseNotebookViews", async () => { @@ -208,7 +207,7 @@ describe("Gallery", () => { const response = await junoClient.increaseNotebookViews(id); expect(response.status).toBe(HttpStatusCodes.OK); - expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/views`, { + expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}/views`, { method: "PATCH", }); }); @@ -225,7 +224,9 @@ describe("Gallery", () => { const authorizationHeader = getAuthorizationHeader(); expect(response.status).toBe(HttpStatusCodes.OK); expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/downloads`, + `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${ + sampleDatabaseAccount.name + }/gallery/${id}/downloads`, { method: "PATCH", headers: { @@ -248,7 +249,9 @@ describe("Gallery", () => { const authorizationHeader = getAuthorizationHeader(); expect(response.status).toBe(HttpStatusCodes.OK); expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/favorite`, + `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${ + sampleDatabaseAccount.name + }/gallery/${id}/favorite`, { method: "PATCH", headers: { @@ -271,7 +274,9 @@ describe("Gallery", () => { const authorizationHeader = getAuthorizationHeader(); expect(response.status).toBe(HttpStatusCodes.OK); expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/unfavorite`, + `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${ + sampleDatabaseAccount.name + }/gallery/${id}/unfavorite`, { method: "PATCH", headers: { @@ -293,7 +298,9 @@ describe("Gallery", () => { const authorizationHeader = getAuthorizationHeader(); expect(response.status).toBe(HttpStatusCodes.OK); expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/favorites`, + `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${ + sampleDatabaseAccount.name + }/gallery/favorites`, { headers: { [authorizationHeader.header]: authorizationHeader.token, @@ -314,7 +321,9 @@ describe("Gallery", () => { const authorizationHeader = getAuthorizationHeader(); expect(response.status).toBe(HttpStatusCodes.OK); expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/published`, + `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${ + sampleDatabaseAccount.name + }/gallery/published`, { headers: { [authorizationHeader.header]: authorizationHeader.token, @@ -336,7 +345,9 @@ describe("Gallery", () => { const authorizationHeader = getAuthorizationHeader(); expect(response.status).toBe(HttpStatusCodes.OK); expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}`, + `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${ + sampleDatabaseAccount.name + }/gallery/${id}`, { method: "DELETE", headers: { @@ -365,7 +376,9 @@ describe("Gallery", () => { const authorizationHeader = getAuthorizationHeader(); expect(response.status).toBe(HttpStatusCodes.OK); expect(window.fetch).toBeCalledWith( - `${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery`, + `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${ + sampleDatabaseAccount.name + }/gallery`, { method: "PUT", headers: { diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 751b40fe7..34020fc2d 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -7,7 +7,6 @@ import { IGitHubResponse } from "../GitHub/GitHubClient"; import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService"; import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; -import { number } from "prop-types"; export interface IJunoResponse { status: number; @@ -484,8 +483,20 @@ 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) { + const error = `${junoEndpoint} not allowed as juno endpoint`; + console.error(error); + throw new Error(error); + } + + return junoEndpoint; + } + private getNotebooksUrl(): string { - return `${configContext.JUNO_ENDPOINT}/api/notebooks`; + return `${JunoClient.getJunoEndpoint()}/api/notebooks`; } private getAccount(): string { @@ -501,7 +512,7 @@ export class JunoClient { } private getAnalyticsUrl(): string { - return `${configContext.JUNO_ENDPOINT}/api/analytics`; + return `${JunoClient.getJunoEndpoint()}/api/analytics`; } private static getHeaders(): HeadersInit { diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 581317564..6921d6bdd 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -13,6 +13,7 @@ export type Features = { readonly enableTtl: boolean; readonly executeSproc: boolean; readonly hostedDataExplorer: boolean; + readonly junoEndpoint?: string; readonly livyEndpoint?: string; readonly notebookBasePath?: string; readonly notebookServerToken?: string; @@ -27,7 +28,8 @@ export type Features = { export function extractFeatures(given = new URLSearchParams(window.location.search)): Features { const downcased = new URLSearchParams(); const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value); - const get = (key: string, defaultValue?: string) => downcased.get("feature." + key) ?? defaultValue; + const get = (key: string, defaultValue?: string) => + downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue; try { new URLSearchParams(window.parent.location.search).forEach(set); @@ -52,6 +54,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableTtl: "true" === get("enablettl"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), + junoEndpoint: get("junoendpoint"), livyEndpoint: get("livyendpoint"), notebookBasePath: get("notebookbasepath"), notebookServerToken: get("notebookservertoken"),