From d3fb5eabdbd207cbeb4146b84517e2861f78a66d Mon Sep 17 00:00:00 2001 From: Senthamil Sindhu Date: Tue, 25 Jun 2024 09:28:08 -0700 Subject: [PATCH] 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,