Add client-side connection-string login for SQL, Tables, and Gremlin

SQL, Tables, and Gremlin now sign data-plane requests client-side with the account key and skip the Portal Backend proxy (generatetoken/accessinputmetadata/authorizationtokens). Adds client-side host/account validation mirroring the backend ValidateHostAndAccount, plus a real CosmosClient connectivity probe that gates opening the Data Explorer. Mongo and Cassandra continue to use the encrypted-token proxy path.
This commit is contained in:
Asier Isayas
2026-08-11 11:14:50 -04:00
parent cdc5569a34
commit 515de88677
11 changed files with 515 additions and 40 deletions
@@ -1,12 +1,20 @@
import { useBoolean } from "@fluentui/react-hooks";
import { userContext } from "UserContext";
import { client } from "Common/CosmosClient";
import { updateUserContext, 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 { HttpHeaders } from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext";
import { AccessInputMetadata } from "../../../Contracts/DataModels";
import { parseConnectionString } from "../Helpers/ConnectionStringParser";
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
import {
extractAccountKeyFromConnectionString,
isDirectConnectionStringLoginApi,
validateDirectConnectionStringLogin,
} from "../HostedUtils";
interface Props {
connectionString: string;
@@ -14,6 +22,7 @@ interface Props {
setEncryptedToken: (token: string) => void;
setConnectionString: (connectionString: string) => void;
setAuthType: (authType: AuthType) => void;
setDirectLoginMetadata: (metadata: AccessInputMetadata) => void;
}
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
@@ -41,12 +50,43 @@ export const isAccountRestrictedForConnectionStringLogin = async (connectionStri
return (await response.text()).toLowerCase() === "true";
};
// Verifies the account key can actually authenticate against the account by making a lightweight read of
// the database account through the same Cosmos client the Data Explorer uses (client-side signing and
// proxy routing). Returns undefined on success, or an error message when the client cannot connect, so
// the caller can block opening the Data Explorer view.
export const validateDirectConnectionStringConnectivity = async (
connectionString: string,
metadata: AccessInputMetadata,
): Promise<string | undefined> => {
const masterKey = extractAccountKeyFromConnectionString(connectionString);
if (!metadata?.documentEndpoint || !masterKey) {
return "Unable to connect to the account with the provided connection string.";
}
// Configure the client the same way the Data Explorer will, then issue a lightweight authenticated
// request. The Cosmos client signs locally with the master key and routes through the same proxy.
updateUserContext({
authType: AuthType.ConnectionString,
masterKey,
endpoint: metadata.documentEndpoint,
refreshCosmosClient: true,
});
try {
await client().getDatabaseAccount();
return undefined;
} catch {
return "Unable to connect to the account. Please verify the connection string is correct and that the account is reachable.";
}
};
export const ConnectExplorer: React.FunctionComponent<Props> = ({
setEncryptedToken,
login,
setAuthType,
connectionString,
setConnectionString,
setDirectLoginMetadata,
}: Props) => {
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
const [errorMessage, setErrorMessage] = React.useState("");
@@ -79,9 +119,39 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
return;
}
const encryptedToken = await fetchEncryptedToken(connectionString);
setEncryptedToken(encryptedToken);
setAuthType(AuthType.ConnectionString);
const metadata = parseConnectionString(connectionString);
if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) {
// SQL, Tables, 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.
// Validate the host and account client-side (mirrors the backend's ValidateHostAndAccount).
const validationError = validateDirectConnectionStringLogin(connectionString, metadata);
if (validationError) {
setErrorMessage(validationError);
return;
}
// Only open the view once we confirm the Cosmos client can actually connect.
const connectivityError = await validateDirectConnectionStringConnectivity(
connectionString,
metadata,
);
if (connectivityError) {
setErrorMessage(connectivityError);
return;
}
setDirectLoginMetadata(metadata);
setAuthType(AuthType.ConnectionString);
return;
}
try {
const encryptedToken = await fetchEncryptedToken(connectionString);
setEncryptedToken(encryptedToken);
setAuthType(AuthType.ConnectionString);
} catch (error) {
setErrorMessage(
"Failed to connect using the provided connection string. Please verify it is correct and try again.",
);
}
}}
>
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>