This commit is contained in:
Senthamil Sindhu 2024-10-04 15:50:19 -07:00
commit ae7184f7ea
5 changed files with 129 additions and 46 deletions

View File

@ -27,7 +27,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
); );
if (!userContext.aadToken) { if (!userContext.aadToken) {
logConsoleError( logConsoleError(
`AAD token does not exist. Please click on "Login for Entra ID" button prior to performing Entra ID RBAC operations`, `AAD token does not exist. Please use the "Login for Entra ID" button in the Toolbar prior to performing Entra ID RBAC operations`,
); );
return null; return null;
} }

View File

@ -10,7 +10,7 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
@ -259,25 +259,8 @@ export default class Explorer {
public async openLoginForEntraIDPopUp(): Promise<void> { public async openLoginForEntraIDPopUp(): Promise<void> {
if (userContext.databaseAccount.properties?.documentEndpoint) { if (userContext.databaseAccount.properties?.documentEndpoint) {
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
/\/$/,
"/.default",
);
const msalInstance = await getMsalInstance();
try { try {
const response = await msalInstance.loginPopup({ const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false);
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 }); updateUserContext({ aadToken: aadToken });
useDataPlaneRbac.setState({ aadTokenUpdated: true }); useDataPlaneRbac.setState({ aadTokenUpdated: true });
} catch (error) { } catch (error) {

View File

@ -1,3 +1,7 @@
import {
AuthError as msalAuthError,
BrowserAuthErrorMessage as msalBrowserAuthErrorMessage,
} from "@azure/msal-browser";
import { import {
Checkbox, Checkbox,
ChoiceGroup, ChoiceGroup,
@ -5,8 +9,6 @@ import {
IChoiceGroupOption, IChoiceGroupOption,
ISpinButtonStyles, ISpinButtonStyles,
IToggleStyles, IToggleStyles,
MessageBar,
MessageBarType,
Position, Position,
SpinButton, SpinButton,
Toggle, Toggle,
@ -30,6 +32,7 @@ import {
} from "Shared/StorageUtility"; } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { updateUserContext, userContext } from "UserContext"; import { updateUserContext, userContext } from "UserContext";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
@ -108,7 +111,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
: Constants.RBACOptions.setAutomaticRBACOption, : Constants.RBACOptions.setAutomaticRBACOption,
); );
const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState<boolean>(false);
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled()); const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold()); const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
@ -203,6 +205,24 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
hasDataPlaneRbacSettingChanged: true, hasDataPlaneRbacSettingChanged: true,
}); });
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
try {
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, true);
updateUserContext({ aadToken: aadToken });
useDataPlaneRbac.setState({ aadTokenUpdated: true });
} catch (authError) {
if (
authError instanceof msalAuthError &&
authError.errorCode === msalBrowserAuthErrorMessage.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 click on "Login for Entra ID" button`,
);
} else {
logConsoleError(
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
);
}
}
} else { } else {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: false, dataPlaneRbacEnabled: false,
@ -347,13 +367,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
option: IChoiceGroupOption, option: IChoiceGroupOption,
): void => { ): void => {
setEnableDataPlaneRBACOption(option.key); 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<HTMLElement>, checked?: boolean): void => { const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
@ -528,17 +541,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={true}
onDismiss={() => setShowDataPlaneRBACWarning(false)}
dismissButtonAriaLabel="Close"
>
Please click on &quot;Login for Entra ID RBAC&quot; button prior to performing Entra ID RBAC
operations
</MessageBar>
)}
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
ID RBAC. ID RBAC.

View File

@ -1,11 +1,12 @@
import * as msal from "@azure/msal-browser"; import * as msal from "@azure/msal-browser";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor"; import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
@ -64,7 +65,83 @@ export async function getMsalInstance() {
return msalInstance; return msalInstance;
} }
export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) { export async function acquireMsalTokenForAccount(
account: DatabaseAccount,
silent: boolean = false,
user_hint?: string,
) {
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
throw new Error("Database account has no document endpoint defined");
}
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
/\/+$/,
"/.default",
);
const msalInstance = await getMsalInstance();
const knownAccounts = msalInstance.getAllAccounts();
// If user_hint is provided, we will try to use it to find the account.
// If no account is found, we will use the current active account or first account in the list.
const msalAccount =
knownAccounts?.filter((account) => account.username === user_hint)[0] ??
msalInstance.getActiveAccount() ??
knownAccounts?.[0];
if (!msalAccount) {
// If no account was found, we need to sign in.
// This will eventually throw InteractionRequiredAuthError if silent is true, we won't handle it here.
const loginRequest = {
scopes: [hrefEndpoint],
loginHint: user_hint,
};
try {
if (silent) {
// We can try to use SSO between different apps to avoid showing a popup.
// With a hint provided, this should work in most cases.
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-sso#sso-between-different-apps
try {
const loginResponse = await msalInstance.ssoSilent(loginRequest);
return loginResponse.accessToken;
} catch (silentError) {
trace(Action.SignInAad, ActionModifiers.Mark, {
request: JSON.stringify(loginRequest),
acquireTokenType: silent ? "silent" : "interactive",
errorMessage: JSON.stringify(silentError),
});
}
}
// If silent acquisition failed, we need to show a popup.
// Passing prompt: "none" will still show a popup but not perform a full sign-in.
// This will only work if the user has already signed in and the session is still valid.
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-prompt-behavior#interactive-requests-with-promptnone
// The hint will be used to pre-fill the username field in the popup if silent is false.
const loginResponse = await msalInstance.loginPopup({ prompt: silent ? "none" : "login", ...loginRequest });
return loginResponse.accessToken;
} catch (error) {
traceFailure(Action.SignInAad, {
request: JSON.stringify(loginRequest),
acquireTokenType: silent ? "silent" : "interactive",
errorMessage: JSON.stringify(error),
});
throw error;
}
} else {
msalInstance.setActiveAccount(msalAccount);
}
const tokenRequest = {
account: msalAccount || null,
forceRefresh: true,
scopes: [hrefEndpoint],
authority: `${configContext.AAD_ENDPOINT}${msalAccount.tenantId}`,
};
return acquireTokenWithMsal(msalInstance, tokenRequest, silent);
}
export async function acquireTokenWithMsal(
msalInstance: msal.IPublicClientApplication,
request: msal.SilentRequest,
silent: boolean = false,
) {
const tokenRequest = { const tokenRequest = {
account: msalInstance.getActiveAccount() || null, account: msalInstance.getActiveAccount() || null,
...request, ...request,
@ -74,7 +151,7 @@ export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientAppli
// attempt silent acquisition first // attempt silent acquisition first
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken; return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
} catch (silentError) { } catch (silentError) {
if (silentError instanceof msal.InteractionRequiredAuthError) { if (silentError instanceof msal.InteractionRequiredAuthError && silent === false) {
try { try {
// The error indicates that we need to acquire the token interactively. // The error indicates that we need to acquire the token interactively.
// This will display a pop-up to re-establish authorization. If user does not // This will display a pop-up to re-establish authorization. If user does not

View File

@ -41,7 +41,12 @@ import {
import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import { acquireTokenWithMsal, getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils"; import {
acquireMsalTokenForAccount,
acquireTokenWithMsal,
getAuthorizationHeader,
getMsalInstance,
} from "../Utils/AuthorizationUtils";
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation"; import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
import { getReadOnlyKeys, listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { getReadOnlyKeys, listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
@ -575,6 +580,22 @@ async function configurePortal(): Promise<Explorer> {
"Explorer/configurePortal", "Explorer/configurePortal",
); );
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name); await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
} else {
Logger.logInfo(
`Trying to silently acquire MSAL token for ${userContext.apiType} account ${account.name}`,
"Explorer/configurePortal",
);
try {
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, true);
updateUserContext({ aadToken: aadToken });
useDataPlaneRbac.setState({ aadTokenUpdated: true });
} catch (authError) {
Logger.logWarning(
`Failed to silently acquire authorization token from MSAL: ${authError} for ${userContext.apiType} account ${account}`,
"Explorer/configurePortal",
);
logConsoleError("Failed to silently acquire authorization token: " + authError);
}
} }
updateUserContext({ dataPlaneRbacEnabled }); updateUserContext({ dataPlaneRbacEnabled });