mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-08-30 10:18:40 +01:00
Improve connection string login feedback
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user