diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index bfecb27c1..3a00cb10e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -435,6 +435,22 @@ export class JunoEndpoints { public static readonly Stage = "https://tools-staging.cosmos.azure.com"; } +export class MongoProxyEndpoints { + public static readonly Development: string = "https://localhost:7238"; + public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; +} + +export class CassandraProxyEndpoints { + public static readonly Development: string = "https://localhost:7240"; + public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn"; +} + export class PriorityLevel { public static readonly High = "high"; public static readonly Low = "low"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 2a4d9fed7..2a3671f7c 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -1,5 +1,9 @@ import { Constants as CosmosSDKConstants } from "@azure/cosmos"; -import { MongoProxyEndpoints, allowedMongoProxyEndpoints_ToBeDeprecated, validateEndpoint } from "Utils/EndpointUtils"; +import { + allowedMongoProxyEndpoints, + allowedMongoProxyEndpoints_ToBeDeprecated, + validateEndpoint, +} from "Utils/EndpointUtils"; import queryString from "querystring"; import { AuthType } from "../AuthType"; import { configContext } from "../ConfigContext"; @@ -10,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId"; import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; -import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants"; +import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants"; import { MinimalQueryIterator } from "./IteratorUtilities"; import { sendMessage } from "./MessageHandler"; @@ -644,7 +648,10 @@ export function getFeatureEndpointOrDefault(feature: string): string { } else { endpoint = hasFlag(userContext.features.mongoProxyAPIs, feature) && - validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints_ToBeDeprecated) + validateEndpoint(userContext.features.mongoProxyEndpoint, [ + ...allowedMongoProxyEndpoints, + ...allowedMongoProxyEndpoints_ToBeDeprecated, + ]) ? userContext.features.mongoProxyEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; } @@ -683,8 +690,14 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter } function useMongoProxyEndpoint(api: string): boolean { + let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; + if (userContext.databaseAccount.properties.ipRules?.length > 0) { + canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; + } + return ( + canAccessMongoProxy && configContext.NEW_MONGO_APIS?.includes(api) && - [MongoProxyEndpoints.Development, MongoProxyEndpoints.MPAC].includes(configContext.MONGO_PROXY_ENDPOINT) + [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT) ); } diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index c04bc9671..3422b200f 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,7 +1,8 @@ -import { JunoEndpoints } from "Common/Constants"; +import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants"; import { allowedAadEndpoints, allowedArcadiaEndpoints, + allowedCassandraProxyEndpoints, allowedEmulatorEndpoints, allowedGraphEndpoints, allowedHostedExplorerEndpoints, @@ -40,7 +41,9 @@ export interface ConfigContext { BACKEND_ENDPOINT?: string; MONGO_BACKEND_ENDPOINT?: string; MONGO_PROXY_ENDPOINT?: string; + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean; NEW_MONGO_APIS?: string[]; + CASSANDRA_PROXY_ENDPOINT?: string; PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; @@ -85,7 +88,7 @@ let configContext: Readonly = { GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: JunoEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", - MONGO_PROXY_ENDPOINT: "https://cdb-ms-prod-mp.cosmos.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ // "resourcelist", // "createDocument", @@ -94,6 +97,8 @@ let configContext: Readonly = { // "deleteDocument", // "createCollectionWithProxy", ], + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, + CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, isTerminalEnabled: false, isPhoenixEnabled: false, }; @@ -147,6 +152,10 @@ export function updateConfigContext(newContext: Partial): void { delete newContext.MONGO_BACKEND_ENDPOINT; } + if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) { + delete newContext.CASSANDRA_PROXY_ENDPOINT; + } + if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) { delete newContext.JUNO_ENDPOINT; } @@ -164,10 +173,7 @@ export function updateConfigContext(newContext: Partial): void { // Injected for local development. These will be removed in the production bundle by webpack if (process.env.NODE_ENV === "development") { - const port: string = process.env.PORT || "1234"; updateConfigContext({ - BACKEND_ENDPOINT: "https://localhost:" + port, - MONGO_BACKEND_ENDPOINT: "https://localhost:" + port, PROXY_PATH: "/proxy", EMULATOR_ENDPOINT: "https://localhost:8081", }); diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 885ebabd0..3bfb62ae1 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -387,6 +387,7 @@ export interface DataExplorerInputsFrame { serverId?: string; extensionEndpoint?: string; mongoProxyEndpoint?: string; + cassandraProxyEndpoint?: string; subscriptionType?: SubscriptionType; quotaId?: string; isTryCosmosDBSubscription?: boolean; diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 6de7aa945..ff12d63f8 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -1,5 +1,8 @@ -import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react"; +import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react"; +import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants"; import { sendMessage } from "Common/MessageHandler"; +import { configContext, updateConfigContext } from "ConfigContext"; +import { IpRule } from "Contracts/DataModels"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; @@ -12,6 +15,7 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility"; import { userContext } from "UserContext"; +import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import ko from "knockout"; import React, { MutableRefObject, useEffect, useRef, useState } from "react"; @@ -33,6 +37,10 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState( userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(), ); + const [ + showMongoAndCassandraProxiesNetworkSettingsWarningState, + setShowMongoAndCassandraProxiesNetworkSettingsWarningState, + ] = useState(showMongoAndCassandraProxiesNetworkSettingsWarning()); return (
{networkSettingsWarning && ( @@ -69,9 +77,25 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { }, }} > - { - "To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove the limit, go to the Settings cog on the right and find 'RU Threshold'." - } + {`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove + the limit, go to the Settings cog on the right and find "RU Threshold".`} + + Learn More + + + )} + {showMongoAndCassandraProxiesNetworkSettingsWarningState && ( + { + setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false); + }} + > + {`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please + re-enable "Allow access from Azure Portal" on the Networking blade for your account.`} )}
@@ -299,3 +323,59 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`); } }; + +const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { + const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; + if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) { + const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; + const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); + const ipRulesIncludeLegacyPortalBackend: boolean = + ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule)) + ?.length === legacyPortalBackendIPs.length; + + if (!ipRulesIncludeLegacyPortalBackend) { + return false; + } + + if (userContext.apiType === "Mongo") { + const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes( + configContext.MONGO_PROXY_ENDPOINT, + ); + + const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint + ? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]] + : MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT]; + + const ipRulesIncludeMongoProxy: boolean = + ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule)) + ?.length === mongoProxyOutboundIPs.length; + + if (ipRulesIncludeMongoProxy) { + updateConfigContext({ + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true, + }); + } + + return !ipRulesIncludeMongoProxy; + } else if (userContext.apiType === "Cassandra") { + const isProdOrMpacCassandraProxyEndpoint: boolean = [ + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + ].includes(configContext.CASSANDRA_PROXY_ENDPOINT); + + const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint + ? [ + ...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac], + ...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod], + ] + : CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT]; + + const ipRulesIncludeCassandraProxy: boolean = + ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule)) + ?.length === cassandraProxyOutboundIPs.length; + + return !ipRulesIncludeCassandraProxy; + } + } + return false; +}; diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index 656f55903..3443d8c71 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -1,4 +1,4 @@ -import { JunoEndpoints } from "Common/Constants"; +import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants"; import * as Logger from "../Common/Logger"; export function validateEndpoint( @@ -67,17 +67,16 @@ export const PortalBackendIPs: { [key: string]: string[] } = { //usnat: ["7.28.202.68"], }; -export class MongoProxyEndpoints { - public static readonly Development: string = "https://localhost:7238"; - public static readonly MPAC: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; - public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; - public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; - public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; -} +export const MongoProxyOutboundIPs: { [key: string]: string[] } = { + [MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"], + [MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"], + [MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"], + [MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"], +}; export const allowedMongoProxyEndpoints: ReadonlyArray = [ MongoProxyEndpoints.Development, - MongoProxyEndpoints.MPAC, + MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod, MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Mooncake, @@ -91,6 +90,21 @@ export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray = "https://localhost:12901", ]; +export const allowedCassandraProxyEndpoints: ReadonlyArray = [ + CassandraProxyEndpoints.Development, + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + CassandraProxyEndpoints.Fairfax, + CassandraProxyEndpoints.Mooncake, +]; + +export const CassandraProxyOutboundIPs: { [key: string]: string[] } = { + [CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"], + [CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"], + [CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"], + [CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"], +}; + export const allowedEmulatorEndpoints: ReadonlyArray = ["https://localhost:8081"]; export const allowedMongoBackendEndpoints: ReadonlyArray = ["https://localhost:1234"]; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index e32b7c3af..e6338e80b 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -480,6 +480,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT, ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint, + CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint, }); updateUserContext({