mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-01 03:08:41 +01:00
Support Sovereign/PPE endpoint zones for connection string login + E2E test for SQL account with public network access disabled (#2568)
* Support sovereign/PPE endpoint zones for connection string login Move the hardcoded account endpoint suffixes out of Constants and into ConfigContext so connection string login works in sovereign clouds and PPE, and widen ConnectionStringParser to accept every configured zone. Surface Portal Backend rejections in the hosted connect form: read the body off the Response that fetchEncryptedToken throws, and offer a firewall help link on a 403. Add an E2E test covering SQL connection string login against an account with public network access disabled. * Harden connection string DNS zone matching Detect PPE accounts from Mongo and Cassandra connection strings, build their document endpoint from the matched zone, match the PPE suffix on a label boundary, and escape every regex metacharacter in config-supplied zones. Drop the sqlx.cosmosdb.azure.com zone, which is not a real SQL zone. * Mock web-vitals globally in test setup ScenarioMonitor subscribes to web-vitals when it is imported, so the onTTFB timer can throw during any suite that outlives it. Mocking it in setupTests.ts keeps that out of individual test files. * Improve connection string login feedback * Preserve connection restriction check order * Flatten connection login error handling --------- Co-authored-by: Asier Isayas <aisayas@microsoft.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { FluentProvider, Link, MessageBar, MessageBarBody, webLightTheme } from "@fluentui/react-components";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { getErrorMessage } from "Common/ErrorHandlingUtils";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import { fetchEncryptedToken, isAccountRestrictedForConnectionStringLogin } from "../../../Common/PortalBackendClient";
|
||||
import { AccessInputMetadata } from "../../../Contracts/DataModels";
|
||||
import { parseConnectionString } from "../Helpers/ConnectionStringParser";
|
||||
@@ -30,6 +31,8 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
}: Props) => {
|
||||
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
|
||||
const [errorMessage, setErrorMessage] = React.useState("");
|
||||
const [isBlockedByFirewall, setIsBlockedByFirewall] = React.useState(false);
|
||||
const [isConnecting, setIsConnecting] = React.useState(false);
|
||||
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin;
|
||||
|
||||
return (
|
||||
@@ -43,39 +46,70 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
{isFormVisible && enableConnectionStringLogin ? (
|
||||
<form
|
||||
id="connectWithConnectionString"
|
||||
aria-busy={isConnecting}
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault();
|
||||
if (isConnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage("");
|
||||
setIsBlockedByFirewall(false);
|
||||
setIsConnecting(true);
|
||||
|
||||
try {
|
||||
if (await isAccountRestrictedForConnectionStringLogin(connectionString)) {
|
||||
setErrorMessage(
|
||||
"This account has been blocked from connection-string login. Please go to cosmos.azure.com/aad for AAD based login.",
|
||||
);
|
||||
setIsConnecting(false);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(getErrorMessage(error));
|
||||
setErrorMessage(getErrorMessage(error as Error));
|
||||
setIsConnecting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isResourceTokenConnectionString(connectionString)) {
|
||||
setAuthType(AuthType.ResourceToken);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isResourceTokenConnectionString(connectionString)) {
|
||||
setAuthType(AuthType.ResourceToken);
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = parseConnectionString(connectionString);
|
||||
if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) {
|
||||
// SQL, Table, and Gremlin sign data-plane requests client-side with the account key, so
|
||||
// we skip the Portal Backend proxy and use the metadata parsed from the connection string.
|
||||
setAccountMetadata(metadata);
|
||||
const metadata = parseConnectionString(connectionString);
|
||||
if (!metadata) {
|
||||
setErrorMessage(
|
||||
"We couldn't recognize this connection string. Verify that it is a valid Azure Cosmos DB connection string and try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDirectConnectionStringLoginApi(metadata.apiKind)) {
|
||||
setAccountMetadata(metadata);
|
||||
setAuthType(AuthType.ConnectionString);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mongo and Cassandra go through the Portal Backend
|
||||
const encryptedToken = await fetchEncryptedToken(connectionString);
|
||||
setEncryptedToken(encryptedToken);
|
||||
setAuthType(AuthType.ConnectionString);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
const errorDetails = await (error as Response).text();
|
||||
|
||||
const encryptedToken = await fetchEncryptedToken(connectionString);
|
||||
setEncryptedToken(encryptedToken);
|
||||
setAuthType(AuthType.ConnectionString);
|
||||
setErrorMessage(
|
||||
errorDetails
|
||||
? `Couldn't authenticate with Cosmos DB: ${errorDetails}`
|
||||
: "Failed to connect to the account. Please check the connection string and try again.",
|
||||
);
|
||||
// A Forbidden usually means the account firewall dropped the request. The connection
|
||||
// string is exchanged by the Portal Backend rather than the browser, so the account has
|
||||
// to allowlist those services.
|
||||
setIsBlockedByFirewall((error as Response).status === HttpStatusCodes.Forbidden);
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
|
||||
@@ -90,15 +124,33 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
setConnectionString(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{errorMessage.length > 0 && (
|
||||
<span className="errorDetailsInfoTooltip">
|
||||
<img className="errorImg" src={ErrorImage} alt="Error notification" />
|
||||
<span className="errorDetails">{errorMessage}</span>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{errorMessage.length > 0 && (
|
||||
<FluentProvider theme={webLightTheme} className="connectErrorMessageBar">
|
||||
<MessageBar intent="error" layout="multiline">
|
||||
<MessageBarBody>
|
||||
<span className="errorDetails">{errorMessage}</span>
|
||||
{isBlockedByFirewall && (
|
||||
<Link
|
||||
className="errorHelpLink"
|
||||
href="https://learn.microsoft.com/azure/cosmos-db/how-to-configure-firewall#allow-requests-from-the-azure-portal"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Allow access from Azure Portal
|
||||
</Link>
|
||||
)}
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
</FluentProvider>
|
||||
)}
|
||||
<p className="connectExplorerContent">
|
||||
<input className="filterbtnstyle" type="submit" value="Connect" />
|
||||
<input
|
||||
className="filterbtnstyle"
|
||||
type="submit"
|
||||
value={isConnecting ? "Connecting..." : "Connect"}
|
||||
disabled={isConnecting}
|
||||
/>
|
||||
</p>
|
||||
<p className="switchConnectTypeText" onClick={login}>
|
||||
Sign In with Azure Account
|
||||
|
||||
Reference in New Issue
Block a user