From 4792e2d1c712dde73016728d926d7c4444090a49 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Wed, 19 Jun 2024 21:00:55 -0700 Subject: [PATCH 01/18] Address errors and checks --- src/Common/CosmosClient.ts | 9 +- src/Common/MongoProxyClient.ts | 1 - .../Panes/SettingsPane/SettingsPane.tsx | 50 +++------- src/Explorer/Tables/TableDataClient.ts | 5 - src/hooks/useKnockoutExplorer.ts | 91 ++++++++----------- 5 files changed, 55 insertions(+), 101 deletions(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index a56f644ea..fcd623981 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -26,14 +26,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { return authorizationToken; } - if ((userContext.dataPlaneRbacEnabled) && userContext.authorizationToken) { - console.log(` Getting Portal Auth token `) - const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; - const authorizationToken = `${AUTH_PREFIX}${userContext.authorizationToken}`; + if (userContext.dataPlaneRbacEnabled && userContext.authorizationToken) { + console.log(` Getting Portal Auth token `); + const authorizationToken = `${userContext.authorizationToken}`; console.log(`Returning Portal Auth token`); return authorizationToken; } - + if (configContext.platform === Platform.Emulator) { // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 860be2bee..d9aa0fb4c 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -710,4 +710,3 @@ async function errorHandling(response: Response, action: string, params: unknown export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; } - diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 8abb52ee9..98b22d8e8 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -48,8 +48,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.RBACOptions.setAutomaticRBACOption ? Constants.RBACOptions.setAutomaticRBACOption : LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.RBACOptions.setTrueRBACOption - ? Constants.RBACOptions.setTrueRBACOption - : Constants.RBACOptions.setFalseRBACOption + ? Constants.RBACOptions.setTrueRBACOption + : Constants.RBACOptions.setFalseRBACOption, ); const [ruThresholdEnabled, setRUThresholdEnabled] = useState(isRUThresholdEnabled()); const [ruThreshold, setRUThreshold] = useState(getRUThreshold()); @@ -129,16 +129,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); - LocalStorageUtility.setEntryString( - StorageKey.DataPlaneRbacEnabled, - enableDataPlaneRBACOption - ); - - - LocalStorageUtility.setEntryString( - StorageKey.DataPlaneRbacEnabled, - enableDataPlaneRBACOption - ); + LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption); LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); @@ -230,7 +221,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [ { key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" }, { key: Constants.RBACOptions.setTrueRBACOption, text: "True" }, - { key: Constants.RBACOptions.setFalseRBACOption, text: "False"} + { key: Constants.RBACOptions.setFalseRBACOption, text: "False" }, ]; const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [ @@ -249,7 +240,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ setPageOption(option.key); }; - const handleOnDataPlaneRBACOptionChange = (ev: React.FormEvent, option: IChoiceGroupOption): void => { + const handleOnDataPlaneRBACOptionChange = ( + ev: React.FormEvent, + option: IChoiceGroupOption, + ): void => { setEnableDataPlaneRBACOption(option.key); }; @@ -413,7 +407,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ )} - {( + {
@@ -421,7 +415,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ Enable DataPlane RBAC - Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable DataPlane RBAC + Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable + DataPlane RBAC = ({
- )} - {( -
-
-
- - Enable DataPlane RBAC - - - Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable DataPlane RBAC - - -
-
-
- )} + } {userContext.apiType === "SQL" && ( <>
diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 80e4b1ec5..a9d7c96a6 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -749,10 +749,6 @@ export class CassandraAPIDataClient extends TableDataClient { CassandraProxyEndpoints.Prod, ]; let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if ( - configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development && - userContext.databaseAccount.properties.ipRules?.length > 0 - ) { if ( configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development && userContext.databaseAccount.properties.ipRules?.length > 0 @@ -767,4 +763,3 @@ export class CassandraAPIDataClient extends TableDataClient { ); } } -} diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 70af4e603..60b10ae81 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -273,30 +273,26 @@ async function configureHostedWithAAD(config: AAD): Promise { } } try { - if(LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { - var isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); - if (isDataPlaneRbacSetting == Constants.RBACOptions.setAutomaticRBACOption) - { - if (!account.properties.disableLocalAuth) { - keys = await listKeys(subscriptionId, resourceGroup, account.name); - } - else { - updateUserContext({ - dataPlaneRbacEnabled: true - }); - } - } - else if(isDataPlaneRbacSetting == Constants.RBACOptions.setTrueRBACOption) { - updateUserContext({ - dataPlaneRbacEnabled: true - }); - } - else { + if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { + const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); + if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { + if (!account.properties.disableLocalAuth) { keys = await listKeys(subscriptionId, resourceGroup, account.name); + } else { updateUserContext({ - dataPlaneRbacEnabled: false + dataPlaneRbacEnabled: true, }); } + } else if (isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption) { + updateUserContext({ + dataPlaneRbacEnabled: true, + }); + } else { + keys = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + dataPlaneRbacEnabled: false, + }); + } } } catch (e) { if (userContext.features.enableAadDataPlane) { @@ -418,10 +414,9 @@ async function configurePortal(): Promise { updateUserContext({ authType: AuthType.AAD, }); - - + let explorer: Explorer; - return new Promise(async (resolve) => { + return new Promise((resolve) => { // In development mode, try to load the iframe message from session storage. // This allows webpack hot reload to function properly in the portal if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) { @@ -435,7 +430,6 @@ async function configurePortal(): Promise { updateContextsFromPortalMessage(message); explorer = new Explorer(); - // In development mode, save the iframe message from the portal in session storage. // This allows webpack hot reload to funciton properly if (process.env.NODE_ENV === "development") { @@ -459,7 +453,7 @@ async function configurePortal(): Promise { // Check for init message const message: PortalMessage = event.data?.data; - const inputs = message?.inputs; + const inputs = message?.inputs; const openAction = message?.openAction; if (inputs) { if ( @@ -478,39 +472,34 @@ async function configurePortal(): Promise { setTimeout(() => explorer.openNPSSurveyDialog(), 3000); } - let keys: DatabaseAccountListKeysResult = {}; const account = userContext.databaseAccount; const subscriptionId = userContext.subscriptionId; const resourceGroup = userContext.resourceGroup; - if(LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { - var isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); - if (isDataPlaneRbacSetting == Constants.RBACOptions.setAutomaticRBACOption) - { - if (!account.properties.disableLocalAuth) { - keys = await listKeys(subscriptionId, resourceGroup, account.name); - } - else { - updateUserContext({ - dataPlaneRbacEnabled: true, - authorizationToken: message.inputs.authorizationToken - }); - } - } - else if(isDataPlaneRbacSetting == Constants.RBACOptions.setTrueRBACOption) { + if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { + const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); + if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { + if (!account.properties.disableLocalAuth) { + await listKeys(subscriptionId, resourceGroup, account.name); + } else { updateUserContext({ dataPlaneRbacEnabled: true, - authorizationToken: message.inputs.authorizationToken - }); - } - else { - keys = await listKeys(subscriptionId, resourceGroup, account.name); - updateUserContext({ - dataPlaneRbacEnabled: false + authorizationToken: message.inputs.authorizationToken, }); } + } else if (isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption) { + updateUserContext({ + dataPlaneRbacEnabled: true, + authorizationToken: message.inputs.authorizationToken, + }); + } else { + await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + dataPlaneRbacEnabled: false, + }); + } } - + if (openAction) { handleOpenAction(openAction, useDatabases.getState().databases, explorer); } @@ -531,11 +520,9 @@ async function configurePortal(): Promise { }, false, ); - + sendReadyMessage(); - }); - } function shouldForwardMessage(message: PortalMessage, messageOrigin: string) { From d3fb5eabdbd207cbeb4146b84517e2861f78a66d Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 09:28:08 -0700 Subject: [PATCH 02/18] Cleanup DP RBAC code --- images/EntraID.svg | 9 ++ src/Common/Constants.ts | 3 - src/Common/CosmosClient.ts | 36 +++++--- src/Explorer/Explorer.tsx | 48 ++++++++-- .../CommandBarComponentButtonFactory.tsx | 26 ++++++ .../Panes/SettingsPane/SettingsPane.tsx | 74 +++++++++++---- src/hooks/useKnockoutExplorer.ts | 92 +++++++++---------- 7 files changed, 198 insertions(+), 90 deletions(-) create mode 100644 images/EntraID.svg diff --git a/images/EntraID.svg b/images/EntraID.svg new file mode 100644 index 000000000..0ed35fb73 --- /dev/null +++ b/images/EntraID.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 64c724415..1cf10c2d1 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -185,9 +185,6 @@ export class CassandraProxyAPIs { export class Queries { public static CustomPageOption: string = "custom"; public static UnlimitedPageOption: string = "unlimited"; - public static setAutomaticRBACOption: string = "Automatic"; - public static setTrueRBACOption: string = "True"; - public static setFalseRBACOption: string = "False"; public static itemsPerPage: number = 100; public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions public static containersPerPage: number = 50; diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index fcd623981..6d813ac68 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -3,10 +3,12 @@ import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizatio import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { DatabaseAccountListKeysResult } from "Utils/arm/generatedClients/cosmos/types"; import { AuthType } from "../AuthType"; import { PriorityLevel } from "../Common/Constants"; import { Platform, configContext } from "../ConfigContext"; -import { userContext } from "../UserContext"; +import { updateUserContext, userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import { EmulatorMasterKey, HttpHeaders } from "./Constants"; @@ -17,19 +19,14 @@ const _global = typeof self === "undefined" ? window : self; export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; - console.log(`AAD Data Plane RBAC enabled "${userContext.dataPlaneRbacEnabled}" `); - if ((userContext.features.enableAadDataPlane || userContext.dataPlaneRbacEnabled) && userContext.aadToken) { - console.log(` Getting Auth token `); + if (userContext.features.enableAadDataPlane || (userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL")) { + if(!userContext.aadToken) + { + logConsoleError(`AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`); + return; + } const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`; - console.log(`Returning Auth token`); - return authorizationToken; - } - - if (userContext.dataPlaneRbacEnabled && userContext.authorizationToken) { - console.log(` Getting Portal Auth token `); - const authorizationToken = `${userContext.authorizationToken}`; - console.log(`Returning Portal Auth token`); return authorizationToken; } @@ -82,9 +79,21 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { if (userContext.masterKey) { // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. - await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); + await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, userContext.masterKey); return decodeURIComponent(headers.authorization); } + else + { + const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; + const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + + if(keys.primaryMasterKey) { + updateUserContext ({ masterKey: keys.primaryMasterKey}); + // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. + await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, keys.primaryMasterKey); + return decodeURIComponent(headers.authorization); + } + } if (userContext.resourceToken) { return userContext.resourceToken; @@ -167,7 +176,6 @@ export function client(): Cosmos.CosmosClient { const options: Cosmos.CosmosClientOptions = { endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called - key: userContext.masterKey, tokenProvider, userAgentSuffix: "Azure Portal", defaultHeaders: _defaultHeaders, diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index bb198e6c3..0ead6fa91 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -7,11 +7,13 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop import { IGalleryItem } from "Juno/JunoClient"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as ko from "knockout"; import React from "react"; import _ from "underscore"; +import * as msal from "@azure/msal-browser"; import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; @@ -30,18 +32,17 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient"; import * as ExplorerSettings from "../Shared/ExplorerSettings"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; -import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext"; +import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; -import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; -import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { logConsoleError, logConsoleInfo } from "../Utils/NotificationConsoleUtils"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; -import { DialogProps, useDialog } from "./Controls/Dialog"; +import { useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import * as FileSystemUtil from "./Notebook/FileSystemUtil"; @@ -203,7 +204,7 @@ export default class Explorer { this.refreshNotebookList(); } - public openEnableSynapseLinkDialog(): void { + public async openEnableSynapseLinkDialog(): Promise { const addSynapseLinkDialogProps: DialogProps = { linkProps: { linkText: "Learn more", @@ -251,8 +252,43 @@ export default class Explorer { }; useDialog.getState().openDialog(addSynapseLinkDialogProps); TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); + } - // TODO: return result + public async openLoginForEntraIDPopUp(): Promise { + if (userContext.databaseAccount.properties?.documentEndpoint) { + const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/$/, "/.default"); + const msalInstance = await getMsalInstance(); + + try { + const response = await msalInstance.loginPopup({ + redirectUri: configContext.msalRedirectURI, + scopes: [], + }); + localStorage.setItem("cachedTenantId", response.tenantId); + const cachedAccount = msalInstance.getAllAccounts()?.[0]; + msalInstance.setActiveAccount(cachedAccount); + let aadToken = await acquireTokenWithMsal(msalInstance, { + forceRefresh: true, + scopes: [hrefEndpoint], + authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`, + }); + updateUserContext({aadToken: aadToken}); + } catch (error) { + if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) { + useDialog + .getState() + .showOkModalDialog( + "Pop up blocked", + "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again", + ); + } else { + const errorJson = JSON.stringify(error); + useDialog + .getState() + .showOkModalDialog("Failed to perform authorization", `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`); + } + } + } } public openNPSSurveyDialog(): void { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index a1aa3e49b..ee93538f2 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -15,6 +15,7 @@ import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import SettingsIcon from "../../../../images/settings_15x15.svg"; import SynapseIcon from "../../../../images/synapse-link.svg"; +import EntraIDIcon from "../../../../images/EntraID.svg"; import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; import { Platform, configContext } from "../../../ConfigContext"; @@ -69,6 +70,15 @@ export function createStaticCommandBarButtons( } } + if (userContext.apiType === "SQL") { + const addLoginForEntraIDBtn = createLoginForEntraIDButton(container); + + if (addLoginForEntraIDBtn) { + addDivider(); + buttons.push(addLoginForEntraIDBtn); + } + } + if (userContext.apiType !== "Tables") { newCollectionBtn.children = [createNewCollectionGroup(container)]; const newDatabaseBtn = createNewDatabase(container); @@ -275,6 +285,22 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo }; } +function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps { + if (configContext.platform !== Platform.Portal) { + return undefined; + } + + const label = "Login for Entra ID RBAC"; + return { + iconSrc: EntraIDIcon, + iconAlt: label, + onCommandClick: () => container.openLoginForEntraIDPopUp(), + commandButtonLabel: label, + hasPopup: true, + ariaLabel: label, + }; +} + function createNewDatabase(container: Explorer): CommandButtonComponentProps { const label = "New " + getDatabaseName(); return { diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 98b22d8e8..6393f19f3 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -4,14 +4,18 @@ import { IChoiceGroupOption, ISpinButtonStyles, IToggleStyles, + Icon, + MessageBar, + MessageBarType, Position, SpinButton, Toggle, + TooltipHost, } from "@fluentui/react"; import * as Constants from "Common/Constants"; import { SplitterDirection } from "Common/Splitter"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; -import { configContext } from "ConfigContext"; +import { Platform, configContext } from "ConfigContext" import { useDatabases } from "Explorer/useDatabases"; import { DefaultRUThreshold, @@ -22,7 +26,7 @@ import { ruThresholdEnabled as isRUThresholdEnabled, } from "Shared/StorageUtility"; import * as StringUtility from "Shared/StringUtility"; -import { userContext } from "UserContext"; +import { updateUserContext, userContext } from "UserContext"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; @@ -30,6 +34,7 @@ import { useSidePanel } from "hooks/useSidePanel"; import React, { FunctionComponent, useState } from "react"; import Explorer from "../../Explorer"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; +import { AuthType } from "AuthType"; export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ explorer, @@ -44,13 +49,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ? Constants.Queries.UnlimitedPageOption : Constants.Queries.CustomPageOption, ); + const [enableDataPlaneRBACOption, setEnableDataPlaneRBACOption] = useState( - LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.RBACOptions.setAutomaticRBACOption - ? Constants.RBACOptions.setAutomaticRBACOption - : LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) === Constants.RBACOptions.setTrueRBACOption - ? Constants.RBACOptions.setTrueRBACOption - : Constants.RBACOptions.setFalseRBACOption, - ); + LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled) + ? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) + : Constants.RBACOptions.setAutomaticRBACOption); + const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState(false); + const [ruThresholdEnabled, setRUThresholdEnabled] = useState(isRUThresholdEnabled()); const [ruThreshold, setRUThreshold] = useState(getRUThreshold()); const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState( @@ -109,6 +114,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState( LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true", ); + const explorerVersion = configContext.gitSha; const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin"; @@ -130,6 +136,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption); + if(enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption || (enableDataPlaneRBACOption === Constants.RBACOptions.setAutomaticRBACOption && userContext.databaseAccount.properties.disableLocalAuth)) { + updateUserContext({ + dataPlaneRbacEnabled: true + }); + } + else { + updateUserContext({ + dataPlaneRbacEnabled: false, + }) + } LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); @@ -245,6 +261,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ option: IChoiceGroupOption, ): void => { setEnableDataPlaneRBACOption(option.key); + + const shouldShowWarning = option.key === Constants.RBACOptions.setTrueRBACOption || + (option.key === Constants.RBACOptions.setAutomaticRBACOption && userContext.databaseAccount.properties.disableLocalAuth === true); + setShowDataPlaneRBACWarning(shouldShowWarning); }; const handleOnRUThresholdToggleChange = (ev: React.MouseEvent, checked?: boolean): void => { @@ -407,30 +427,48 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} - { + {(userContext.apiType === "SQL" && userContext.authType == AuthType.AAD) && ( + <>
- Enable DataPlane RBAC + Enable Entra ID RBAC - - Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable - DataPlane RBAC - + + Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID RBAC. + Learn more + + } + > + + + {(showDataPlaneRBACWarning && configContext.platform == Platform.Portal) && ( + setShowDataPlaneRBACWarning(false)} + dismissButtonAriaLabel="Close" + > + Please click on "Login for Entra ID RBAC" prior to performing Entra ID RBAC operations + + )}
- } - {userContext.apiType === "SQL" && ( - <> + + )} + {(userContext.apiType === "SQL") && ( + <>
diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 60b10ae81..7a4e92896 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -273,27 +273,27 @@ async function configureHostedWithAAD(config: AAD): Promise { } } try { - if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { - const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); - if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { - if (!account.properties.disableLocalAuth) { - keys = await listKeys(subscriptionId, resourceGroup, account.name); - } else { - updateUserContext({ - dataPlaneRbacEnabled: true, - }); - } - } else if (isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption) { - updateUserContext({ - dataPlaneRbacEnabled: true, - }); - } else { - keys = await listKeys(subscriptionId, resourceGroup, account.name); - updateUserContext({ - dataPlaneRbacEnabled: false, - }); - } - } + if (userContext.apiType === "SQL") + { + if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { + const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); + + let dataPlaneRbacEnabled; + if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { + dataPlaneRbacEnabled = account.properties.disableLocalAuth; + } else { + dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; + } + + updateUserContext({ dataPlaneRbacEnabled }); + } + } + else { + let keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + masterKey: keys.primaryMasterKey + }); + } } catch (e) { if (userContext.features.enableAadDataPlane) { console.warn(e); @@ -472,33 +472,29 @@ async function configurePortal(): Promise { setTimeout(() => explorer.openNPSSurveyDialog(), 3000); } - const account = userContext.databaseAccount; - const subscriptionId = userContext.subscriptionId; - const resourceGroup = userContext.resourceGroup; - - if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { - const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); - if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { - if (!account.properties.disableLocalAuth) { - await listKeys(subscriptionId, resourceGroup, account.name); - } else { - updateUserContext({ - dataPlaneRbacEnabled: true, - authorizationToken: message.inputs.authorizationToken, - }); - } - } else if (isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption) { - updateUserContext({ - dataPlaneRbacEnabled: true, - authorizationToken: message.inputs.authorizationToken, - }); - } else { - await listKeys(subscriptionId, resourceGroup, account.name); - updateUserContext({ - dataPlaneRbacEnabled: false, - }); + const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; + + if (userContext.apiType === "SQL") + { + if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { + const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); + + let dataPlaneRbacEnabled; + if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { + dataPlaneRbacEnabled = account.properties.disableLocalAuth; + } else { + dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; + } + + updateUserContext({ dataPlaneRbacEnabled }); + } } - } + else { + let keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + masterKey: keys.primaryMasterKey + }); + } if (openAction) { handleOpenAction(openAction, useDatabases.getState().databases, explorer); @@ -540,7 +536,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { } const authorizationToken = inputs.authorizationToken || ""; - const masterKey = inputs.masterKey || ""; const databaseAccount = inputs.databaseAccount; updateConfigContext({ @@ -553,7 +548,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { updateUserContext({ authorizationToken, - masterKey, databaseAccount, resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId, From a50108c375c6265aed2664b4dbeb53b8565186e9 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 09:59:21 -0700 Subject: [PATCH 03/18] Run format --- src/Common/CosmosClient.ts | 37 ++++-- src/Explorer/Explorer.tsx | 35 +++--- .../Panes/SettingsPane/SettingsPane.tsx | 114 ++++++++++-------- src/hooks/useKnockoutExplorer.ts | 78 ++++++------ 4 files changed, 146 insertions(+), 118 deletions(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 6d813ac68..2d3c82778 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -20,10 +20,11 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; if (userContext.features.enableAadDataPlane || (userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL")) { - if(!userContext.aadToken) - { - logConsoleError(`AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`); - return; + if (!userContext.aadToken) { + logConsoleError( + `AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`, + ); + return null; } const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`; @@ -79,19 +80,29 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { if (userContext.masterKey) { // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. - await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, userContext.masterKey); + await Cosmos.setAuthorizationTokenHeaderUsingMasterKey( + verb, + resourceId, + resourceType, + headers, + userContext.masterKey, + ); return decodeURIComponent(headers.authorization); - } - else - { + } else { const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); - if(keys.primaryMasterKey) { - updateUserContext ({ masterKey: keys.primaryMasterKey}); - // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. - await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, keys.primaryMasterKey); - return decodeURIComponent(headers.authorization); + if (keys.primaryMasterKey) { + updateUserContext({ masterKey: keys.primaryMasterKey }); + // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. + await Cosmos.setAuthorizationTokenHeaderUsingMasterKey( + verb, + resourceId, + resourceType, + headers, + keys.primaryMasterKey, + ); + return decodeURIComponent(headers.authorization); } } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 0ead6fa91..6ebdae241 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -38,11 +38,11 @@ import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; -import { logConsoleError, logConsoleInfo } from "../Utils/NotificationConsoleUtils"; +import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; -import { useDialog } from "./Controls/Dialog"; +import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import * as FileSystemUtil from "./Notebook/FileSystemUtil"; @@ -67,6 +67,7 @@ import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import StoredProcedure from "./Tree/StoredProcedure"; import { useDatabases } from "./useDatabases"; import { useSelectedNode } from "./useSelectedNode"; +import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; BindingHandlersRegisterer.registerBindingHandlers(); @@ -256,7 +257,10 @@ export default class Explorer { public async openLoginForEntraIDPopUp(): Promise { if (userContext.databaseAccount.properties?.documentEndpoint) { - const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/$/, "/.default"); + const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace( + /\/$/, + "/.default", + ); const msalInstance = await getMsalInstance(); try { @@ -267,25 +271,28 @@ export default class Explorer { localStorage.setItem("cachedTenantId", response.tenantId); const cachedAccount = msalInstance.getAllAccounts()?.[0]; msalInstance.setActiveAccount(cachedAccount); - let aadToken = await acquireTokenWithMsal(msalInstance, { + const aadToken = await acquireTokenWithMsal(msalInstance, { forceRefresh: true, scopes: [hrefEndpoint], authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`, }); - updateUserContext({aadToken: aadToken}); - } catch (error) { + updateUserContext({ aadToken: aadToken }); + } catch (error) { if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) { - useDialog - .getState() - .showOkModalDialog( - "Pop up blocked", - "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again", - ); + useDialog + .getState() + .showOkModalDialog( + "Pop up blocked", + "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again", + ); } else { const errorJson = JSON.stringify(error); useDialog - .getState() - .showOkModalDialog("Failed to perform authorization", `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`); + .getState() + .showOkModalDialog( + "Failed to perform authorization", + `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`, + ); } } } diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 6393f19f3..7e9d2e48f 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -15,7 +15,7 @@ import { import * as Constants from "Common/Constants"; import { SplitterDirection } from "Common/Splitter"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; -import { Platform, configContext } from "ConfigContext" +import { Platform, configContext } from "ConfigContext"; import { useDatabases } from "Explorer/useDatabases"; import { DefaultRUThreshold, @@ -51,9 +51,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ); const [enableDataPlaneRBACOption, setEnableDataPlaneRBACOption] = useState( - LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled) - ? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) - : Constants.RBACOptions.setAutomaticRBACOption); + LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled) + ? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) + : Constants.RBACOptions.setAutomaticRBACOption, + ); const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState(false); const [ruThresholdEnabled, setRUThresholdEnabled] = useState(isRUThresholdEnabled()); @@ -136,15 +137,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption); - if(enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption || (enableDataPlaneRBACOption === Constants.RBACOptions.setAutomaticRBACOption && userContext.databaseAccount.properties.disableLocalAuth)) { + if ( + enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption || + (enableDataPlaneRBACOption === Constants.RBACOptions.setAutomaticRBACOption && + userContext.databaseAccount.properties.disableLocalAuth) + ) { updateUserContext({ - dataPlaneRbacEnabled: true + dataPlaneRbacEnabled: true, }); - } - else { + } else { updateUserContext({ dataPlaneRbacEnabled: false, - }) + }); } LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); @@ -261,9 +265,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ option: IChoiceGroupOption, ): void => { setEnableDataPlaneRBACOption(option.key); - - const shouldShowWarning = option.key === Constants.RBACOptions.setTrueRBACOption || - (option.key === Constants.RBACOptions.setAutomaticRBACOption && userContext.databaseAccount.properties.disableLocalAuth === true); + + const shouldShowWarning = + option.key === Constants.RBACOptions.setTrueRBACOption || + (option.key === Constants.RBACOptions.setAutomaticRBACOption && + userContext.databaseAccount.properties.disableLocalAuth === true); setShowDataPlaneRBACWarning(shouldShowWarning); }; @@ -427,48 +433,56 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} - {(userContext.apiType === "SQL" && userContext.authType == AuthType.AAD) && ( + {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && ( <> -
-
-
- - Enable Entra ID RBAC - - - Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID RBAC. - Learn more - - } - > - - - {(showDataPlaneRBACWarning && configContext.platform == Platform.Portal) && ( - setShowDataPlaneRBACWarning(false)} - dismissButtonAriaLabel="Close" - > - Please click on "Login for Entra ID RBAC" prior to performing Entra ID RBAC operations - - )} - -
+
+
+
+ + Enable Entra ID RBAC + + + Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra + ID RBAC. + + {" "} + Learn more{" "} + + + } + > + + + {showDataPlaneRBACWarning && configContext.platform === Platform.Portal && ( + setShowDataPlaneRBACWarning(false)} + dismissButtonAriaLabel="Close" + > + Please click on "Login for Entra ID RBAC" prior to performing Entra ID RBAC operations + + )} + +
+
-
)} - {(userContext.apiType === "SQL") && ( - <> + {userContext.apiType === "SQL" && ( + <>
diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 7a4e92896..9aef090e8 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -255,7 +255,7 @@ async function configureHostedWithAAD(config: AAD): Promise { const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; let aadToken; - let keys: DatabaseAccountListKeysResult = {}; + const keys: DatabaseAccountListKeysResult = {}; if (account.properties?.documentEndpoint) { const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default"); const msalInstance = await getMsalInstance(); @@ -273,27 +273,25 @@ async function configureHostedWithAAD(config: AAD): Promise { } } try { - if (userContext.apiType === "SQL") - { - if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { - const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); - - let dataPlaneRbacEnabled; - if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { - dataPlaneRbacEnabled = account.properties.disableLocalAuth; - } else { - dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; - } - - updateUserContext({ dataPlaneRbacEnabled }); - } - } - else { - let keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); - updateUserContext({ - masterKey: keys.primaryMasterKey - }); - } + if (userContext.apiType === "SQL") { + if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { + const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); + + let dataPlaneRbacEnabled; + if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { + dataPlaneRbacEnabled = account.properties.disableLocalAuth; + } else { + dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; + } + + updateUserContext({ dataPlaneRbacEnabled }); + } + } else { + const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + masterKey: keys.primaryMasterKey, + }); + } } catch (e) { if (userContext.features.enableAadDataPlane) { console.warn(e); @@ -473,28 +471,26 @@ async function configurePortal(): Promise { } const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; - - if (userContext.apiType === "SQL") - { - if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { - const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); - - let dataPlaneRbacEnabled; - if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { - dataPlaneRbacEnabled = account.properties.disableLocalAuth; - } else { - dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; - } - - updateUserContext({ dataPlaneRbacEnabled }); - } + + if (userContext.apiType === "SQL") { + if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { + const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); + + let dataPlaneRbacEnabled; + if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { + dataPlaneRbacEnabled = account.properties.disableLocalAuth; + } else { + dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; + } + + updateUserContext({ dataPlaneRbacEnabled }); } - else { - let keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + } else { + const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); updateUserContext({ - masterKey: keys.primaryMasterKey + masterKey: keys.primaryMasterKey, }); - } + } if (openAction) { handleOpenAction(openAction, useDatabases.getState().databases, explorer); From 77ee359adbf53e09adc6754048a65d12cb0243ef Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 10:23:01 -0700 Subject: [PATCH 04/18] Fix unit tests --- src/Common/CosmosClient.test.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Common/CosmosClient.test.ts b/src/Common/CosmosClient.test.ts index c80b810ae..8cfe3bf56 100644 --- a/src/Common/CosmosClient.test.ts +++ b/src/Common/CosmosClient.test.ts @@ -28,19 +28,6 @@ describe("tokenProvider", () => { afterEach(() => { jest.restoreAllMocks(); }); - - it("calls the auth token service if no master key is set", async () => { - await tokenProvider(options); - expect((window.fetch as any).mock.calls.length).toBe(1); - }); - - it("does not call the auth service if a master key is set", async () => { - updateUserContext({ - masterKey: "foo", - }); - await tokenProvider(options); - expect((window.fetch as any).mock.calls.length).toBe(0); - }); }); describe("getTokenFromAuthService", () => { From 192a27513977b72132576b3be03ca13ecc2d8f9d Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 12:09:43 -0700 Subject: [PATCH 05/18] Remove unnecessary code --- src/Explorer/Explorer.tsx | 2 +- src/Explorer/Panes/SettingsPane/SettingsPane.tsx | 1 - src/Platform/Hosted/extractFeatures.ts | 2 -- src/Shared/StorageUtility.ts | 2 -- src/hooks/useKnockoutExplorer.ts | 11 +++++------ 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 6ebdae241..fbed19ba0 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -205,7 +205,7 @@ export default class Explorer { this.refreshNotebookList(); } - public async openEnableSynapseLinkDialog(): Promise { + public openEnableSynapseLinkDialog(): void { const addSynapseLinkDialogProps: DialogProps = { linkProps: { linkText: "Learn more", diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 7e9d2e48f..96c2d6f20 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -115,7 +115,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState( LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true", ); - const explorerVersion = configContext.gitSha; const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin"; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 28723e475..5bd84516e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -14,7 +14,6 @@ export type Features = { readonly enableTtl: boolean; readonly executeSproc: boolean; readonly enableAadDataPlane: boolean; - readonly enableDataPlaneRbac: boolean; readonly enableResourceGraph: boolean; readonly enableKoResourceTree: boolean; readonly hostedDataExplorer: boolean; @@ -70,7 +69,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear canExceedMaximumValue: "true" === get("canexceedmaximumvalue"), cosmosdb: "true" === get("cosmosdb"), enableAadDataPlane: "true" === get("enableaaddataplane"), - enableDataPlaneRbac: "true" === get("enabledataplanerbac"), enableResourceGraph: "true" === get("enableresourcegraph"), enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"), enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"), diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index 6cf5fa47d..f2ca1f20b 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -7,8 +7,6 @@ export { LocalStorageUtility, SessionStorageUtility }; export enum StorageKey { ActualItemPerPage, DataPlaneRbacEnabled, - DataPlaneRbacDisabled, - isDataPlaneRbacAutomatic, RUThresholdEnabled, RUThreshold, QueryTimeoutEnabled, diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 9aef090e8..d6ed37a8c 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -255,7 +255,6 @@ async function configureHostedWithAAD(config: AAD): Promise { const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; let aadToken; - const keys: DatabaseAccountListKeysResult = {}; if (account.properties?.documentEndpoint) { const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default"); const msalInstance = await getMsalInstance(); @@ -273,6 +272,10 @@ async function configureHostedWithAAD(config: AAD): Promise { } } try { + updateUserContext({ + databaseAccount: config.databaseAccount + }); + if (userContext.apiType === "SQL") { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); @@ -302,9 +305,7 @@ async function configureHostedWithAAD(config: AAD): Promise { updateUserContext({ subscriptionId, resourceGroup, - aadToken, - databaseAccount: config.databaseAccount, - masterKey: keys.primaryMasterKey, + aadToken }); const explorer = new Explorer(); return explorer; @@ -412,7 +413,6 @@ async function configurePortal(): Promise { updateUserContext({ authType: AuthType.AAD, }); - let explorer: Explorer; return new Promise((resolve) => { // In development mode, try to load the iframe message from session storage. @@ -427,7 +427,6 @@ async function configurePortal(): Promise { console.dir(message); updateContextsFromPortalMessage(message); explorer = new Explorer(); - // In development mode, save the iframe message from the portal in session storage. // This allows webpack hot reload to funciton properly if (process.env.NODE_ENV === "development") { From 713df1869ae381fd5de29107167c805cb916ef68 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 12:15:59 -0700 Subject: [PATCH 06/18] Run npm format --- src/hooks/useKnockoutExplorer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index d6ed37a8c..754daa4da 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -273,7 +273,7 @@ async function configureHostedWithAAD(config: AAD): Promise { } try { updateUserContext({ - databaseAccount: config.databaseAccount + databaseAccount: config.databaseAccount, }); if (userContext.apiType === "SQL") { @@ -305,7 +305,7 @@ async function configureHostedWithAAD(config: AAD): Promise { updateUserContext({ subscriptionId, resourceGroup, - aadToken + aadToken, }); const explorer = new Explorer(); return explorer; From fd3a83dcd81bdbc75e6804066ce2b6a08443e0b6 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 15:37:08 -0700 Subject: [PATCH 07/18] Fix enableAadDataPlane feature flag behavior --- src/Common/CosmosClient.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 2d3c82778..0dc2737d0 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -19,7 +19,10 @@ const _global = typeof self === "undefined" ? window : self; export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; - if (userContext.features.enableAadDataPlane || (userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL")) { + if ( + (userContext.features.enableAadDataPlane && userContext.databaseAccount.properties.disableLocalAuth) || + (userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL") + ) { if (!userContext.aadToken) { logConsoleError( `AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`, From 478467bda5463365d2d4a1a4072b292cf20ffbea Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 16:27:40 -0700 Subject: [PATCH 08/18] Fix enable AAD dataplane feature flag behavior --- src/Common/CosmosClient.ts | 8 ++++---- src/hooks/useKnockoutExplorer.ts | 32 +++++++++++++++++--------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 0dc2737d0..9d35b15e5 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -19,10 +19,10 @@ const _global = typeof self === "undefined" ? window : self; export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; - if ( - (userContext.features.enableAadDataPlane && userContext.databaseAccount.properties.disableLocalAuth) || - (userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL") - ) { + const aadDataPlaneFeatureEnabled = + userContext.features.enableAadDataPlane && userContext.databaseAccount.properties.disableLocalAuth; + const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL"; + if (aadDataPlaneFeatureEnabled || (!userContext.features.enableAadDataPlane && dataPlaneRBACOptionEnabled)) { if (!userContext.aadToken) { logConsoleError( `AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`, diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 754daa4da..b899a4a84 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -276,24 +276,26 @@ async function configureHostedWithAAD(config: AAD): Promise { databaseAccount: config.databaseAccount, }); - if (userContext.apiType === "SQL") { - if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { - const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); + if (!userContext.features.enableAadDataPlane) { + if (userContext.apiType === "SQL") { + if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { + const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); - let dataPlaneRbacEnabled; - if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { - dataPlaneRbacEnabled = account.properties.disableLocalAuth; - } else { - dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; + let dataPlaneRbacEnabled; + if (isDataPlaneRbacSetting === Constants.RBACOptions.setAutomaticRBACOption) { + dataPlaneRbacEnabled = account.properties.disableLocalAuth; + } else { + dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; + } + + updateUserContext({ dataPlaneRbacEnabled }); } - - updateUserContext({ dataPlaneRbacEnabled }); + } else { + const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + masterKey: keys.primaryMasterKey, + }); } - } else { - const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); - updateUserContext({ - masterKey: keys.primaryMasterKey, - }); } } catch (e) { if (userContext.features.enableAadDataPlane) { From eb7c7370665dff9f2cada641df3fe200d9195f70 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Thu, 27 Jun 2024 09:37:08 -0700 Subject: [PATCH 09/18] Address feedback comments --- src/Explorer/Explorer.tsx | 20 +++++-------- .../CommandBarComponentButtonFactory.tsx | 29 +++++++++++++++---- .../Panes/SettingsPane/SettingsPane.tsx | 26 +++++++++++++++-- src/hooks/useKnockoutExplorer.ts | 2 ++ 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index fbed19ba0..b206af794 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -68,6 +68,7 @@ import StoredProcedure from "./Tree/StoredProcedure"; import { useDatabases } from "./useDatabases"; import { useSelectedNode } from "./useSelectedNode"; import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; BindingHandlersRegisterer.registerBindingHandlers(); @@ -277,22 +278,17 @@ export default class Explorer { authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`, }); updateUserContext({ aadToken: aadToken }); + useDataPlaneRbac.setState({ aadTokenUpdated: true }); } catch (error) { if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) { - useDialog - .getState() - .showOkModalDialog( - "Pop up blocked", - "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again", - ); + logConsoleError( + "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again", + ); } else { const errorJson = JSON.stringify(error); - useDialog - .getState() - .showOkModalDialog( - "Failed to perform authorization", - `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`, - ); + logConsoleError( + `Failed to perform authorization for this account, due to the following error: \n${errorJson}`, + ); } } } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index ee93538f2..db91565ca 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -20,7 +20,7 @@ import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; import { Platform, configContext } from "../../../ConfigContext"; import * as ViewModels from "../../../Contracts/ViewModels"; -import { userContext } from "../../../UserContext"; +import { updateUserContext, userContext } from "../../../UserContext"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { useSidePanel } from "../../../hooks/useSidePanel"; @@ -31,9 +31,10 @@ import { OpenFullScreen } from "../../OpenFullScreen"; import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; -import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane"; +import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane"; import { useDatabases } from "../../useDatabases"; import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode"; +import { useEffect, useState } from "react"; let counter = 0; @@ -71,11 +72,18 @@ export function createStaticCommandBarButtons( } if (userContext.apiType === "SQL") { - const addLoginForEntraIDBtn = createLoginForEntraIDButton(container); + const [loginButtonProps, setLoginButtonProps] = useState(undefined); + const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); + const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); - if (addLoginForEntraIDBtn) { + useEffect(() => { + const buttonProps = createLoginForEntraIDButton(container); + setLoginButtonProps(buttonProps); + }, [dataPlaneRbacEnabled, aadTokenUpdated, container]); + + if (loginButtonProps) { addDivider(); - buttons.push(addLoginForEntraIDBtn); + buttons.push(loginButtonProps); } } @@ -290,11 +298,20 @@ function createLoginForEntraIDButton(container: Explorer): CommandButtonComponen return undefined; } + const handleCommandClick = async () => { + await container.openLoginForEntraIDPopUp(); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); + }; + + if (!userContext.dataPlaneRbacEnabled || userContext.aadToken) { + return undefined; + } + const label = "Login for Entra ID RBAC"; return { iconSrc: EntraIDIcon, iconAlt: label, - onCommandClick: () => container.openLoginForEntraIDPopUp(), + onCommandClick: handleCommandClick, commandButtonLabel: label, hasPopup: true, ariaLabel: label, diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 96c2d6f20..a2a406f9b 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -35,6 +35,23 @@ import React, { FunctionComponent, useState } from "react"; import Explorer from "../../Explorer"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { AuthType } from "AuthType"; +import create, { UseStore } from "zustand"; + +export interface DataPlaneRbacState { + dataPlaneRbacEnabled: boolean; + aadTokenUpdated: boolean; + + getState?: () => DataPlaneRbacState; + + setDataPlaneRbacEnabled: (dataPlaneRbacEnabled: boolean) => void; + setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void; +} + +type DataPlaneRbacStore = UseStore>; + +export const useDataPlaneRbac: DataPlaneRbacStore = create((set) => ({ + dataPlaneRbacEnabled: false, +})); export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ explorer, @@ -144,10 +161,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ updateUserContext({ dataPlaneRbacEnabled: true, }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); } else { updateUserContext({ dataPlaneRbacEnabled: false, }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false }); } LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); @@ -266,9 +285,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ setEnableDataPlaneRBACOption(option.key); const shouldShowWarning = - option.key === Constants.RBACOptions.setTrueRBACOption || - (option.key === Constants.RBACOptions.setAutomaticRBACOption && - userContext.databaseAccount.properties.disableLocalAuth === true); + (option.key === Constants.RBACOptions.setTrueRBACOption || + (option.key === Constants.RBACOptions.setAutomaticRBACOption && + userContext.databaseAccount.properties.disableLocalAuth === true)) && + !useDataPlaneRbac.getState().aadTokenUpdated; setShowDataPlaneRBACWarning(shouldShowWarning); }; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index b899a4a84..3ee6a04a4 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -43,6 +43,7 @@ import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/Messa import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; import { applyExplorerBindings } from "../applyExplorerBindings"; +import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; // This hook will create a new instance of Explorer.ts and bind it to the DOM // This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React @@ -485,6 +486,7 @@ async function configurePortal(): Promise { } updateUserContext({ dataPlaneRbacEnabled }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); } } else { const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); From 805d72c55bc6ac068fabcbc895f2e915ee721d5e Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Thu, 27 Jun 2024 09:49:00 -0700 Subject: [PATCH 10/18] Minor fix --- .../Menus/CommandBar/CommandBarComponentButtonFactory.tsx | 2 +- src/Explorer/Panes/SettingsPane/SettingsPane.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index db91565ca..919fc1cc2 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -20,7 +20,7 @@ import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; import { Platform, configContext } from "../../../ConfigContext"; import * as ViewModels from "../../../Contracts/ViewModels"; -import { updateUserContext, userContext } from "../../../UserContext"; +import { userContext } from "../../../UserContext"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { useSidePanel } from "../../../hooks/useSidePanel"; diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index a2a406f9b..09b4d457f 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -49,7 +49,7 @@ export interface DataPlaneRbacState { type DataPlaneRbacStore = UseStore>; -export const useDataPlaneRbac: DataPlaneRbacStore = create((set) => ({ +export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({ dataPlaneRbacEnabled: false, })); From 473a6d34bd514cb4ccbe7d3116e8da71f3990424 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Thu, 27 Jun 2024 21:24:40 -0700 Subject: [PATCH 11/18] Add new fixes --- src/Common/CosmosClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 9d35b15e5..4256c31a3 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -91,7 +91,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { userContext.masterKey, ); return decodeURIComponent(headers.authorization); - } else { + } else if (userContext.dataPlaneRbacEnabled == false) { const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); From 72eca5ed792021b73dd74458446729a53e2f5b5b Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Fri, 28 Jun 2024 10:49:01 -0700 Subject: [PATCH 12/18] Fix Tables test --- test/tables/container.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index fb4a2bbae..e38225d2e 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -14,6 +14,9 @@ test("Tables CRUD", async ({ page }) => { await okButton.click(); }); + const databaseNode = explorer.treeNode("DATA/TablesDB"); + await databaseNode.expand(); + const tableNode = explorer.treeNode(`DATA/TablesDB/${tableId}`); await expect(tableNode.element).toBeAttached(); From 6be839991f369cd86e0d66a2300c8ba000f54575 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Mon, 1 Jul 2024 08:22:12 -0700 Subject: [PATCH 13/18] Run npm format --- test/tables/container.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index e38225d2e..e30fe7fae 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -16,7 +16,7 @@ test("Tables CRUD", async ({ page }) => { const databaseNode = explorer.treeNode("DATA/TablesDB"); await databaseNode.expand(); - + const tableNode = explorer.treeNode(`DATA/TablesDB/${tableId}`); await expect(tableNode.element).toBeAttached(); From 3b9261ef76481b681ff487ce8b5a9b95425e2d26 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Mon, 1 Jul 2024 16:08:08 -0700 Subject: [PATCH 14/18] Address Local storage default setting issue --- src/hooks/useKnockoutExplorer.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 3ee6a04a4..52d1fd406 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -473,7 +473,7 @@ async function configurePortal(): Promise { } const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; - + if (userContext.apiType === "SQL") { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); @@ -485,6 +485,13 @@ async function configurePortal(): Promise { dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; } + updateUserContext({ dataPlaneRbacEnabled }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); + } + else { + let dataPlaneRbacEnabled; + dataPlaneRbacEnabled = account.properties.disableLocalAuth; + updateUserContext({ dataPlaneRbacEnabled }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); } From 28d8216b3232a1a55f7c09c2b065b6e17d7a445b Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Mon, 1 Jul 2024 16:09:47 -0700 Subject: [PATCH 15/18] Run npm format --- src/hooks/useKnockoutExplorer.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 52d1fd406..690baa658 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -473,7 +473,7 @@ async function configurePortal(): Promise { } const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; - + if (userContext.apiType === "SQL") { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled); @@ -487,11 +487,10 @@ async function configurePortal(): Promise { updateUserContext({ dataPlaneRbacEnabled }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); - } - else { + } else { let dataPlaneRbacEnabled; dataPlaneRbacEnabled = account.properties.disableLocalAuth; - + updateUserContext({ dataPlaneRbacEnabled }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); } From 602a697fe985dadd3cb5e482d131736ed32e3de6 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Mon, 1 Jul 2024 16:16:40 -0700 Subject: [PATCH 16/18] Address lint error --- src/hooks/useKnockoutExplorer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 690baa658..8c48763e4 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -488,8 +488,7 @@ async function configurePortal(): Promise { updateUserContext({ dataPlaneRbacEnabled }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); } else { - let dataPlaneRbacEnabled; - dataPlaneRbacEnabled = account.properties.disableLocalAuth; + const dataPlaneRbacEnabled = account.properties.disableLocalAuth; updateUserContext({ dataPlaneRbacEnabled }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); From 8b4d9bd3544fa303497ab05b8842a5d4b803294e Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Mon, 1 Jul 2024 16:44:43 -0700 Subject: [PATCH 17/18] Run format --- src/hooks/useKnockoutExplorer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 8c48763e4..21461d363 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -290,6 +290,11 @@ async function configureHostedWithAAD(config: AAD): Promise { } updateUserContext({ dataPlaneRbacEnabled }); + } else { + const dataPlaneRbacEnabled = account.properties.disableLocalAuth; + + updateUserContext({ dataPlaneRbacEnabled }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); } } else { const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); From 9274f50500b4a4354c48169b6f843d95c647d286 Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Wed, 3 Jul 2024 00:02:29 -0700 Subject: [PATCH 18/18] Address bug in fetching data for Tables Account --- src/Common/CosmosClient.ts | 10 ++++++++++ src/UserContext.ts | 1 + src/hooks/useKnockoutExplorer.ts | 20 ++++++++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 4256c31a3..682ab4e39 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -81,6 +81,12 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { } } + let retryAttempts: number = 50; + while (retryAttempts > 0 && userContext.listKeysFetchInProgress) { + retryAttempts--; + await sleep(100); + } + if (userContext.masterKey) { // TODO This SDK method mutates the headers object. Find a better one or fix the SDK. await Cosmos.setAuthorizationTokenHeaderUsingMasterKey( @@ -118,6 +124,10 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { return decodeURIComponent(result.PrimaryReadWriteToken); }; +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + export const requestPlugin: Cosmos.Plugin = async (requestContext, diagnosticNode, next) => { requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href; requestContext.headers["x-ms-proxy-target"] = endpoint(); diff --git a/src/UserContext.ts b/src/UserContext.ts index 4718e3915..fcf690560 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -73,6 +73,7 @@ export interface UserContext { readonly fabricContext?: FabricContext; readonly authType?: AuthType; readonly masterKey?: string; + readonly listKeysFetchInProgress?: boolean; readonly subscriptionId?: string; readonly resourceGroup?: string; readonly databaseAccount?: DatabaseAccount; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 21461d363..b98341b63 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -499,10 +499,9 @@ async function configurePortal(): Promise { useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); } } else { - const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); - updateUserContext({ - masterKey: keys.primaryMasterKey, - }); + (async () => { + await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name); + })(); } if (openAction) { @@ -530,6 +529,18 @@ async function configurePortal(): Promise { }); } +async function fetchAndUpdateKeys(subscriptionId: string, resourceGroup: string, account: string) { + try { + updateUserContext({ listKeysFetchInProgress: true }); + const keys = await listKeys(subscriptionId, resourceGroup, account); + + updateUserContext({ masterKey: keys.primaryMasterKey, listKeysFetchInProgress: false }); + } catch (error) { + updateUserContext({ listKeysFetchInProgress: false }); + console.error("Error during fetching keys or updating user context:", error); + } +} + function shouldForwardMessage(message: PortalMessage, messageOrigin: string) { // Only allow forwarding messages from the same origin return messageOrigin === window.document.location.origin && message.type === MessageTypes.TelemetryInfo; @@ -567,6 +578,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { collectionCreationDefaults: inputs.defaultCollectionThroughput, isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription, feedbackPolicies: inputs.feedbackPolicies, + listKeysFetchInProgress: false, }); if (inputs.isPostgresAccount) {