mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-19 17:12:47 +01:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user