Improve connection string login feedback

This commit is contained in:
Asier Isayas
2026-08-27 11:03:49 -04:00
parent ea056bfaeb
commit 5fe07c2523
2 changed files with 113 additions and 35 deletions
@@ -80,6 +80,63 @@ it("hides the connection string link when feature.disableConnectionStringLogin i
updateUserContext({ features: oldFeatures }); updateUserContext({ features: oldFeatures });
}); });
it("rejects an unrecognized connection string before token exchange", async () => {
render(
<ConnectExplorer
{...{
login: jest.fn(),
setEncryptedToken: jest.fn(),
setAuthType: jest.fn(),
connectionString: "not-a-valid-connection-string",
setConnectionString: jest.fn(),
setAccountMetadata: jest.fn(),
}}
/>,
);
fireEvent.click(screen.getByText("Connect to your account with connection string"));
fireEvent.click(screen.getByDisplayValue("Connect"));
expect(
await screen.findByText(
"We couldn't recognize this connection string. Verify that it is a valid Azure Cosmos DB connection string and try again.",
),
).toBeInTheDocument();
expect(mockIsAccountRestricted).not.toHaveBeenCalled();
expect(mockFetchEncryptedToken).not.toHaveBeenCalled();
});
it("shows that a connection is in progress", async () => {
let finishRestrictionCheck: (restricted: boolean) => void = () => undefined;
mockIsAccountRestricted.mockImplementation(
() =>
new Promise((resolve) => {
finishRestrictionCheck = resolve;
}),
);
render(
<ConnectExplorer
{...{
login: jest.fn(),
setEncryptedToken: jest.fn(),
setAuthType: jest.fn(),
connectionString: "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=some-key;",
setConnectionString: jest.fn(),
setAccountMetadata: jest.fn(),
}}
/>,
);
fireEvent.click(screen.getByText("Connect to your account with connection string"));
fireEvent.click(screen.getByDisplayValue("Connect"));
const connectButton = screen.getByDisplayValue("Connecting...");
expect(connectButton).toBeDisabled();
expect(connectButton.closest("form")).toHaveAttribute("aria-busy", "true");
finishRestrictionCheck(false);
expect(await screen.findByDisplayValue("Connect")).toBeEnabled();
});
it("shows the error when the Portal Backend rejects the connection string", async () => { it("shows the error when the Portal Backend rejects the connection string", async () => {
// Mongo and Cassandra are the APIs that still exchange the connection string for an encrypted token. // Mongo and Cassandra are the APIs that still exchange the connection string for an encrypted token.
const mongoConnectionString = "mongodb://test:key@test.documents.azure.com:10255"; const mongoConnectionString = "mongodb://test:key@test.documents.azure.com:10255";
@@ -32,6 +32,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
const [isFormVisible, { setTrue: showForm }] = useBoolean(false); const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
const [errorMessage, setErrorMessage] = React.useState(""); const [errorMessage, setErrorMessage] = React.useState("");
const [isBlockedByFirewall, setIsBlockedByFirewall] = React.useState(false); const [isBlockedByFirewall, setIsBlockedByFirewall] = React.useState(false);
const [isConnecting, setIsConnecting] = React.useState(false);
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin; const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin;
return ( return (
@@ -45,54 +46,69 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
{isFormVisible && enableConnectionStringLogin ? ( {isFormVisible && enableConnectionStringLogin ? (
<form <form
id="connectWithConnectionString" id="connectWithConnectionString"
aria-busy={isConnecting}
onSubmit={async (event) => { onSubmit={async (event) => {
event.preventDefault(); event.preventDefault();
if (isConnecting) {
return;
}
setErrorMessage(""); setErrorMessage("");
setIsBlockedByFirewall(false); setIsBlockedByFirewall(false);
setIsConnecting(true);
try { try {
if (await isAccountRestrictedForConnectionStringLogin(connectionString)) { if (isResourceTokenConnectionString(connectionString)) {
setAuthType(AuthType.ResourceToken);
return;
}
const metadata = parseConnectionString(connectionString);
if (!metadata) {
setErrorMessage( setErrorMessage(
"This account has been blocked from connection-string login. Please go to cosmos.azure.com/aad for AAD based login.", "We couldn't recognize this connection string. Verify that it is a valid Azure Cosmos DB connection string and try again.",
); );
return; return;
} }
} catch (error) {
setErrorMessage(getErrorMessage(error));
return;
}
if (isResourceTokenConnectionString(connectionString)) { try {
setAuthType(AuthType.ResourceToken); if (await isAccountRestrictedForConnectionStringLogin(connectionString)) {
return; setErrorMessage(
} "This account has been blocked from connection-string login. Please go to cosmos.azure.com/aad for AAD based login.",
);
return;
}
} catch (error) {
setErrorMessage(getErrorMessage(error as Error));
return;
}
const metadata = parseConnectionString(connectionString); if (isDirectConnectionStringLoginApi(metadata.apiKind)) {
if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) { setAccountMetadata(metadata);
// SQL, Table, and Gremlin sign data-plane requests client-side with the account key, so setAuthType(AuthType.ConnectionString);
// we skip the Portal Backend proxy and use the metadata parsed from the connection string. return;
setAccountMetadata(metadata); }
setAuthType(AuthType.ConnectionString);
return;
}
// Mongo and Cassandra go through the Portal Backend // Mongo and Cassandra go through the Portal Backend
try { try {
const encryptedToken = await fetchEncryptedToken(connectionString); const encryptedToken = await fetchEncryptedToken(connectionString);
setEncryptedToken(encryptedToken); setEncryptedToken(encryptedToken);
setAuthType(AuthType.ConnectionString); setAuthType(AuthType.ConnectionString);
} catch (error) { } catch (error) {
const errorDetails = await (error as Response).text(); const errorDetails = await (error as Response).text();
setErrorMessage( setErrorMessage(
errorDetails errorDetails
? `Couldn't authenticate with Cosmos DB: ${errorDetails}` ? `Couldn't authenticate with Cosmos DB: ${errorDetails}`
: "Failed to connect to the account. Please check the connection string and try again.", : "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 // 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 // string is exchanged by the Portal Backend rather than the browser, so the account has
// to allowlist those services. // to allowlist those services.
setIsBlockedByFirewall((error as Response).status === HttpStatusCodes.Forbidden); setIsBlockedByFirewall((error as Response).status === HttpStatusCodes.Forbidden);
}
} finally {
setIsConnecting(false);
} }
}} }}
> >
@@ -129,7 +145,12 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
</FluentProvider> </FluentProvider>
)} )}
<p className="connectExplorerContent"> <p className="connectExplorerContent">
<input className="filterbtnstyle" type="submit" value="Connect" /> <input
className="filterbtnstyle"
type="submit"
value={isConnecting ? "Connecting..." : "Connect"}
disabled={isConnecting}
/>
</p> </p>
<p className="switchConnectTypeText" onClick={login}> <p className="switchConnectTypeText" onClick={login}>
Sign In with Azure Account Sign In with Azure Account