Cleanup DP RBAC code

This commit is contained in:
Senthamil Sindhu 2024-06-25 09:28:08 -07:00
parent 4792e2d1c7
commit d3fb5eabdb
7 changed files with 198 additions and 90 deletions

9
images/EntraID.svg Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="uuid-f8d4d392-7c12-4bd9-baff-66fbf7814b91" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<path d="m3.802,14.032c.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128v4.073c-.286,0-.574-.078-.824-.234l-4.374-2.734Z" fill="#225086"/>
<path d="m7.853,1.507L.353,9.967c-.579.654-.428,1.642.323,2.111,0,0,2.776,1.735,3.126,1.954.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128-4.364-2.728,4.365-4.924V1s0,0,0,0c-.424,0-.847.169-1.147.507Z" fill="#6df"/>
<polygon points="4.636 10.199 4.688 10.231 9 12.927 9.001 12.927 9.001 12.927 9.001 5.276 9 5.275 4.636 10.199" fill="#cbf8ff"/>
<path d="m17.324,12.078c.751-.469.902-1.457.323-2.111l-4.921-5.551c-.397-.185-.842-.291-1.313-.291-.925,0-1.752.399-2.302,1.026l-.109.123h0s4.364,4.924,4.364,4.924h0s0,0,0,0l-4.365,2.728v4.073c.287,0,.573-.078.823-.234l7.5-4.688Z" fill="#074793"/>
<path d="m9.001,1v4.275s.109-.123.109-.123c.55-.627,1.377-1.026,2.302-1.026.472,0,.916.107,1.313.291l-2.579-2.909c-.299-.338-.723-.507-1.146-.507Z" fill="#0294e4"/>
<polygon points="13.365 10.199 13.365 10.199 13.365 10.199 9.001 5.276 9.001 12.926 13.365 10.199" fill="#96bcc2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -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;

View File

@ -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,

View File

@ -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<void> {
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<void> {
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 {

View File

@ -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 {

View File

@ -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<string>(
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<boolean>(false);
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
@ -109,6 +114,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
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<HTMLElement>, checked?: boolean): void => {
@ -407,30 +427,48 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</div>
</div>
)}
{
{(userContext.apiType === "SQL" && userContext.authType == AuthType.AAD) && (
<>
<div className="settingsSection">
<div className="settingsSectionPart">
<fieldset>
<legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
Enable DataPlane RBAC
Enable Entra ID RBAC
</legend>
<InfoTooltip>
Choose Automatic to enable DataPlane RBAC automatically. True/False to voluntarily enable/disable
DataPlane RBAC
</InfoTooltip>
<TooltipHost
content={
<>
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID RBAC.
<a href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer" target="_blank" rel="noopener noreferrer"> Learn more </a>
</>
}
>
<Icon iconName="Info" ariaLabel="Info tooltip" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
{(showDataPlaneRBACWarning && configContext.platform == Platform.Portal) && (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={true}
onDismiss={() => setShowDataPlaneRBACWarning(false)}
dismissButtonAriaLabel="Close"
>
Please click on "Login for Entra ID RBAC" prior to performing Entra ID RBAC operations
</MessageBar>
)}
<ChoiceGroup
ariaLabelledBy="enableDataPlaneRBACOptions"
selectedKey={enableDataPlaneRBACOption}
options={dataPlaneRBACOptionsList}
styles={choiceButtonStyles}
selectedKey={enableDataPlaneRBACOption}
onChange={handleOnDataPlaneRBACOptionChange}
/>
</fieldset>
</div>
</div>
}
{userContext.apiType === "SQL" && (
<>
</>
)}
{(userContext.apiType === "SQL") && (
<>
<div className="settingsSection">
<div className="settingsSectionPart">
<div>

View File

@ -273,27 +273,27 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
}
}
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,32 +472,28 @@ async function configurePortal(): Promise<Explorer> {
setTimeout(() => explorer.openNPSSurveyDialog(), 3000);
}
const account = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
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,
});
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,
authorizationToken: message.inputs.authorizationToken,
});
} else {
await listKeys(subscriptionId, resourceGroup, account.name);
updateUserContext({
dataPlaneRbacEnabled: false,
});
}
else {
let keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name);
updateUserContext({
masterKey: keys.primaryMasterKey
});
}
if (openAction) {
@ -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,