diff --git a/src/Platform/Hosted/Components/ConnectExplorer.test.tsx b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx index 9a6a7bcab..bdc562820 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.test.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx @@ -80,6 +80,63 @@ it("hides the connection string link when feature.disableConnectionStringLogin i updateUserContext({ features: oldFeatures }); }); +it("rejects an unrecognized connection string before token exchange", async () => { + render( + , + ); + 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( + , + ); + 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 () => { // 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"; diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index d83637419..26c60e1d5 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -32,6 +32,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ 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 ( @@ -45,54 +46,69 @@ export const ConnectExplorer: React.FunctionComponent = ({ {isFormVisible && enableConnectionStringLogin ? (
{ event.preventDefault(); + if (isConnecting) { + return; + } + setErrorMessage(""); setIsBlockedByFirewall(false); + setIsConnecting(true); try { - if (await isAccountRestrictedForConnectionStringLogin(connectionString)) { + if (isResourceTokenConnectionString(connectionString)) { + setAuthType(AuthType.ResourceToken); + return; + } + + const metadata = parseConnectionString(connectionString); + if (!metadata) { 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; } - } catch (error) { - setErrorMessage(getErrorMessage(error)); - return; - } - if (isResourceTokenConnectionString(connectionString)) { - setAuthType(AuthType.ResourceToken); - return; - } + 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.", + ); + return; + } + } catch (error) { + setErrorMessage(getErrorMessage(error as Error)); + 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); - setAuthType(AuthType.ConnectionString); - return; - } + if (isDirectConnectionStringLoginApi(metadata.apiKind)) { + setAccountMetadata(metadata); + setAuthType(AuthType.ConnectionString); + return; + } - // Mongo and Cassandra go through the Portal Backend - try { - const encryptedToken = await fetchEncryptedToken(connectionString); - setEncryptedToken(encryptedToken); - setAuthType(AuthType.ConnectionString); - } catch (error) { - const errorDetails = await (error as Response).text(); + // Mongo and Cassandra go through the Portal Backend + try { + const encryptedToken = await fetchEncryptedToken(connectionString); + setEncryptedToken(encryptedToken); + setAuthType(AuthType.ConnectionString); + } catch (error) { + const errorDetails = await (error as Response).text(); - 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); + 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); } }} > @@ -129,7 +145,12 @@ export const ConnectExplorer: React.FunctionComponent = ({ )}

- +

Sign In with Azure Account