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 c5fc473e4..cde5e9462 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -186,9 +186,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.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", () => { diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index a56f644ea..9d35b15e5 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,23 +19,21 @@ 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 `); + 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`, + ); + return null; + } 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 AUTH_PREFIX = `type=aad&ver=1.0&sig=`; - const authorizationToken = `${AUTH_PREFIX}${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); @@ -83,8 +83,30 @@ 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) { @@ -168,7 +190,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/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/Explorer.tsx b/src/Explorer/Explorer.tsx index bb198e6c3..b206af794 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,14 +32,13 @@ 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 { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; @@ -66,6 +67,8 @@ 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"; +import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; BindingHandlersRegisterer.registerBindingHandlers(); @@ -251,8 +254,44 @@ 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); + const aadToken = await acquireTokenWithMsal(msalInstance, { + forceRefresh: true, + scopes: [hrefEndpoint], + 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) { + 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); + logConsoleError( + `Failed to perform 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..919fc1cc2 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"; @@ -30,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; @@ -69,6 +71,22 @@ export function createStaticCommandBarButtons( } } + if (userContext.apiType === "SQL") { + const [loginButtonProps, setLoginButtonProps] = useState(undefined); + const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); + const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); + + useEffect(() => { + const buttonProps = createLoginForEntraIDButton(container); + setLoginButtonProps(buttonProps); + }, [dataPlaneRbacEnabled, aadTokenUpdated, container]); + + if (loginButtonProps) { + addDivider(); + buttons.push(loginButtonProps); + } + } + if (userContext.apiType !== "Tables") { newCollectionBtn.children = [createNewCollectionGroup(container)]; const newDatabaseBtn = createNewDatabase(container); @@ -275,6 +293,31 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo }; } +function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps { + if (configContext.platform !== Platform.Portal) { + 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: handleCommandClick, + 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 8abb52ee9..09b4d457f 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,24 @@ 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"; +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(() => ({ + dataPlaneRbacEnabled: false, +})); export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ explorer, @@ -44,13 +66,14 @@ 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( @@ -129,16 +152,22 @@ 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); + if ( + enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption || + (enableDataPlaneRBACOption === Constants.RBACOptions.setAutomaticRBACOption && + userContext.databaseAccount.properties.disableLocalAuth) + ) { + updateUserContext({ + dataPlaneRbacEnabled: true, + }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); + } else { + updateUserContext({ + dataPlaneRbacEnabled: false, + }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false }); + } LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); @@ -230,7 +259,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,8 +278,18 @@ 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); + + const shouldShowWarning = + (option.key === Constants.RBACOptions.setTrueRBACOption || + (option.key === Constants.RBACOptions.setAutomaticRBACOption && + userContext.databaseAccount.properties.disableLocalAuth === true)) && + !useDataPlaneRbac.getState().aadTokenUpdated; + setShowDataPlaneRBACWarning(shouldShowWarning); }; const handleOnRUThresholdToggleChange = (ev: React.MouseEvent, checked?: boolean): void => { @@ -413,47 +452,53 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ )} - {( -
-
-
- - Enable DataPlane RBAC - - - Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable DataPlane RBAC - - -
+ {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 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 e40ddef6f..25a43f56b 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -755,10 +755,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 @@ -773,4 +769,3 @@ export class CassandraAPIDataClient extends TableDataClient { ); } } -} 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 70af4e603..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 @@ -255,7 +256,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; - let keys: DatabaseAccountListKeysResult = {}; if (account.properties?.documentEndpoint) { const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default"); const msalInstance = await getMsalInstance(); @@ -273,30 +273,30 @@ 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 - }); + updateUserContext({ + databaseAccount: config.databaseAccount, + }); + + 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; } + + updateUserContext({ dataPlaneRbacEnabled }); } - else if(isDataPlaneRbacSetting == Constants.RBACOptions.setTrueRBACOption) { - updateUserContext({ - dataPlaneRbacEnabled: true - }); - } - else { - keys = await listKeys(subscriptionId, resourceGroup, account.name); - updateUserContext({ - dataPlaneRbacEnabled: false - }); - } + } else { + const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + masterKey: keys.primaryMasterKey, + }); + } } } catch (e) { if (userContext.features.enableAadDataPlane) { @@ -309,8 +309,6 @@ async function configureHostedWithAAD(config: AAD): Promise { subscriptionId, resourceGroup, aadToken, - databaseAccount: config.databaseAccount, - masterKey: keys.primaryMasterKey, }); const explorer = new Explorer(); return explorer; @@ -418,10 +416,8 @@ 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")) { @@ -434,8 +430,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") { @@ -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,29 @@ async function configurePortal(): Promise { setTimeout(() => explorer.openNPSSurveyDialog(), 3000); } - let keys: DatabaseAccountListKeysResult = {}; - const account = userContext.databaseAccount; - const subscriptionId = userContext.subscriptionId; - const resourceGroup = userContext.resourceGroup; + const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; - 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) { - updateUserContext({ - dataPlaneRbacEnabled: true, - authorizationToken: message.inputs.authorizationToken - }); - } - 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 }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); + } + } else { + const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + masterKey: keys.primaryMasterKey, + }); } - + if (openAction) { handleOpenAction(openAction, useDatabases.getState().databases, explorer); } @@ -531,11 +515,9 @@ async function configurePortal(): Promise { }, false, ); - + sendReadyMessage(); - }); - } function shouldForwardMessage(message: PortalMessage, messageOrigin: string) { @@ -553,7 +535,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { } const authorizationToken = inputs.authorizationToken || ""; - const masterKey = inputs.masterKey || ""; const databaseAccount = inputs.databaseAccount; updateConfigContext({ @@ -566,7 +547,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { updateUserContext({ authorizationToken, - masterKey, databaseAccount, resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId,