From b07fa89a2363984cbe3d2312bd415b29ee76959e Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Wed, 26 Jun 2024 14:33:57 -0400 Subject: [PATCH 1/5] fix legacy mongo shell regression (#1883) Co-authored-by: Asier Isayas --- .../Tabs/MongoShellTab/MongoShellTabComponent.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index 052c81ce6..61907c90a 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -1,6 +1,5 @@ import { useMongoProxyEndpoint } from "Common/MongoProxyClient"; import React, { Component } from "react"; -import * as Constants from "../../../Common/Constants"; import { configContext } from "../../../ConfigContext"; import * as ViewModels from "../../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; @@ -113,12 +112,6 @@ export default class MongoShellTabComponent extends Component< const resourceId = databaseAccount?.id; const accountName = databaseAccount?.name; const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint; - const mongoEndpoint = - documentEndpoint.substr( - Constants.MongoDBAccounts.protocol.length + 3, - documentEndpoint.length - - (Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length), - ) + Constants.MongoDBAccounts.defaultPort.toString(); const databaseId = this.props.collection.databaseId; const collectionId = this.props.collection.id(); const apiEndpoint = this._useMongoProxyEndpoint @@ -132,7 +125,7 @@ export default class MongoShellTabComponent extends Component< data: { resourceId: resourceId, accountName: accountName, - mongoEndpoint: mongoEndpoint, + mongoEndpoint: documentEndpoint, authorization: authorization, databaseId: databaseId, collectionId: collectionId, From 17754cba05e4d1a156b536dad388f17d83a0ff9b Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 27 Jun 2024 14:40:05 -0400 Subject: [PATCH 2/5] Revert to old Mongo Proxy (#1886) * revert to old mongo proxy * revert to old mongo proxy * cleanup * cleanup --------- Co-authored-by: Asier Isayas --- src/ConfigContext.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 564e905ba..8db033788 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -109,14 +109,14 @@ let configContext: Readonly = { PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ - "resourcelist", - "queryDocuments", - "createDocument", - "readDocument", - "updateDocument", - "deleteDocument", - "createCollectionWithProxy", - "legacyMongoShell", + // "resourcelist", + // "queryDocuments", + // "createDocument", + // "readDocument", + // "updateDocument", + // "deleteDocument", + // "createCollectionWithProxy", + // "legacyMongoShell", ], MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, From c30a9681fed4debaee362ad452cfb75cd16dae05 Mon Sep 17 00:00:00 2001 From: sindhuba <122321535+sindhuba@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:33:07 -0700 Subject: [PATCH 3/5] Add Data Plane RBAC functionality (#1878) * Fix API endpoint for CassandraProxy query API * activate Mongo Proxy and Cassandra Proxy in Prod * Add CP Prod endpoint * Run npm format and tests * Revert code * fix bug that blocked local mongo proxy and cassandra proxy development * Add prod endpoint * fix pr check tests * Remove prod * Remove prod endpoint * Remove dev endpoint * Support data plane RBAC * Support data plane RBAC * Add additional changes for Portal RBAC functionality * Address errors and checks * Cleanup DP RBAC code * Run format * Fix unit tests * Remove unnecessary code * Run npm format * Fix enableAadDataPlane feature flag behavior * Fix enable AAD dataplane feature flag behavior * Address feedback comments * Minor fix * Add new fixes * Fix Tables test * Run npm format --------- Co-authored-by: Asier Isayas --- images/EntraID.svg | 9 ++ src/Common/Constants.ts | 6 + src/Common/CosmosClient.test.ts | 13 -- src/Common/CosmosClient.ts | 40 +++++- src/Explorer/Explorer.tsx | 45 ++++++- .../CommandBarComponentButtonFactory.tsx | 45 ++++++- .../Panes/SettingsPane/SettingsPane.tsx | 121 +++++++++++++++++- src/Shared/StorageUtility.ts | 1 + src/UserContext.ts | 1 + src/hooks/useKnockoutExplorer.ts | 59 +++++++-- test/tables/container.spec.ts | 3 + 11 files changed, 312 insertions(+), 31 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 cb4c8f007..cde5e9462 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -197,6 +197,12 @@ export class Queries { public static readonly DefaultMaxWaitTimeInSeconds = 30; } +export class RBACOptions { + public static setAutomaticRBACOption: string = "Automatic"; + public static setTrueRBACOption: string = "True"; + public static setFalseRBACOption: string = "False"; +} + export class SavedQueries { public static readonly CollectionName: string = "___Query"; public static readonly DatabaseName: string = "___Cosmos"; 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 c24680f4b..4256c31a3 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,7 +19,16 @@ 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.aadToken) { + 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}`; return authorizationToken; @@ -72,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 if (userContext.dataPlaneRbacEnabled == false) { + 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) { @@ -157,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/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 320a10015..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,6 +66,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ? Constants.Queries.UnlimitedPageOption : Constants.Queries.CustomPageOption, ); + + const [enableDataPlaneRBACOption, setEnableDataPlaneRBACOption] = useState( + 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( @@ -119,7 +149,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ StorageKey.ActualItemPerPage, isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage, ); + 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, + }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); + } else { + updateUserContext({ + dataPlaneRbacEnabled: false, + }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false }); + } + LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts); @@ -207,6 +256,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ { key: Constants.PriorityLevel.High, text: "High" }, ]; + const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [ + { key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" }, + { key: Constants.RBACOptions.setTrueRBACOption, text: "True" }, + { key: Constants.RBACOptions.setFalseRBACOption, text: "False" }, + ]; + const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [ { key: SplitterDirection.Vertical, text: "Vertical" }, { key: SplitterDirection.Horizontal, text: "Horizontal" }, @@ -223,6 +278,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ setPageOption(option.key); }; + 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 => { setRUThresholdEnabled(checked); }; @@ -383,6 +452,54 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ )} + {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 + + )} + +
+
+
+ + )} {userContext.apiType === "SQL" && ( <>
diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index 652451e57..f2ca1f20b 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -6,6 +6,7 @@ import * as StringUtility from "./StringUtility"; export { LocalStorageUtility, SessionStorageUtility }; export enum StorageKey { ActualItemPerPage, + DataPlaneRbacEnabled, RUThresholdEnabled, RUThreshold, QueryTimeoutEnabled, diff --git a/src/UserContext.ts b/src/UserContext.ts index 30a0236dc..4718e3915 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -101,6 +101,7 @@ export interface UserContext { sampleDataConnectionInfo?: ParsedResourceTokenConnectionString; readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams; readonly feedbackPolicies?: AdminFeedbackPolicySettings; + readonly dataPlaneRbacEnabled?: boolean; } export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo"; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 3a6061acd..3ee6a04a4 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -1,3 +1,4 @@ +import * as Constants from "Common/Constants"; import { createUri } from "Common/UrlUtility"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; @@ -5,6 +6,7 @@ import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesCon import Explorer from "Explorer/Explorer"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; +import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; @@ -41,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 @@ -253,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(); @@ -271,8 +273,30 @@ async function configureHostedWithAAD(config: AAD): Promise { } } try { - if (!account.properties.disableLocalAuth) { - keys = await listKeys(subscriptionId, resourceGroup, account.name); + 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 { + const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name); + updateUserContext({ + masterKey: keys.primaryMasterKey, + }); + } } } catch (e) { if (userContext.features.enableAadDataPlane) { @@ -285,8 +309,6 @@ async function configureHostedWithAAD(config: AAD): Promise { subscriptionId, resourceGroup, aadToken, - databaseAccount: config.databaseAccount, - masterKey: keys.primaryMasterKey, }); const explorer = new Explorer(); return explorer; @@ -420,7 +442,7 @@ async function configurePortal(): Promise { // In the Portal, configuration of Explorer happens via iframe message window.addEventListener( "message", - (event) => { + async (event) => { if (isInvalidParentFrameOrigin(event)) { return; } @@ -450,6 +472,29 @@ async function configurePortal(): Promise { setTimeout(() => explorer.openNPSSurveyDialog(), 3000); } + 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 }); + 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); } @@ -490,7 +535,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { } const authorizationToken = inputs.authorizationToken || ""; - const masterKey = inputs.masterKey || ""; const databaseAccount = inputs.databaseAccount; updateConfigContext({ @@ -503,7 +547,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { updateUserContext({ authorizationToken, - masterKey, databaseAccount, resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId, diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index fb4a2bbae..e30fe7fae 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 1021e9c96907cab56038ae2e21400a43c4e7897f Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Mon, 1 Jul 2024 12:40:07 -0400 Subject: [PATCH 4/5] Fix LMS regression when using old backend (#1890) Co-authored-by: Asier Isayas --- .../Tabs/MongoShellTab/MongoShellTabComponent.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index 61907c90a..bcf8b7d05 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -1,5 +1,6 @@ import { useMongoProxyEndpoint } from "Common/MongoProxyClient"; import React, { Component } from "react"; +import * as Constants from "../../../Common/Constants"; import { configContext } from "../../../ConfigContext"; import * as ViewModels from "../../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; @@ -112,6 +113,12 @@ export default class MongoShellTabComponent extends Component< const resourceId = databaseAccount?.id; const accountName = databaseAccount?.name; const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint; + const mongoEndpoint = + documentEndpoint.substr( + Constants.MongoDBAccounts.protocol.length + 3, + documentEndpoint.length - + (Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length), + ) + Constants.MongoDBAccounts.defaultPort.toString(); const databaseId = this.props.collection.databaseId; const collectionId = this.props.collection.id(); const apiEndpoint = this._useMongoProxyEndpoint @@ -125,7 +132,7 @@ export default class MongoShellTabComponent extends Component< data: { resourceId: resourceId, accountName: accountName, - mongoEndpoint: documentEndpoint, + mongoEndpoint: this._useMongoProxyEndpoint ? documentEndpoint : mongoEndpoint, authorization: authorization, databaseId: databaseId, collectionId: collectionId, From dfe79b20f5f71247143bed46f4ec2c2731726578 Mon Sep 17 00:00:00 2001 From: sindhuba <122321535+sindhuba@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:58:05 -0700 Subject: [PATCH 5/5] Address RBAC local storage default setting issue (#1892) * Fix API endpoint for CassandraProxy query API * activate Mongo Proxy and Cassandra Proxy in Prod * Add CP Prod endpoint * Run npm format and tests * Revert code * fix bug that blocked local mongo proxy and cassandra proxy development * Add prod endpoint * fix pr check tests * Remove prod * Remove prod endpoint * Remove dev endpoint * Support data plane RBAC * Support data plane RBAC * Add additional changes for Portal RBAC functionality * Address errors and checks * Cleanup DP RBAC code * Run format * Fix unit tests * Remove unnecessary code * Run npm format * Fix enableAadDataPlane feature flag behavior * Fix enable AAD dataplane feature flag behavior * Address feedback comments * Minor fix * Add new fixes * Fix Tables test * Run npm format * Address Local storage default setting issue * Run npm format * Address lint error * Run format --------- Co-authored-by: Asier Isayas --- src/hooks/useKnockoutExplorer.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 3ee6a04a4..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); @@ -485,6 +490,11 @@ async function configurePortal(): Promise { dataPlaneRbacEnabled = isDataPlaneRbacSetting === Constants.RBACOptions.setTrueRBACOption; } + updateUserContext({ dataPlaneRbacEnabled }); + useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); + } else { + const dataPlaneRbacEnabled = account.properties.disableLocalAuth; + updateUserContext({ dataPlaneRbacEnabled }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: dataPlaneRbacEnabled }); }