mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 15:06:55 +00:00
Merge branch 'master' of https://github.com/Azure/cosmos-explorer
This commit is contained in:
commit
ae7184f7ea
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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 "Login for Entra ID RBAC" 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.
|
||||||
|
@ -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
|
||||||
|
@ -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 });
|
||||||
|
Loading…
Reference in New Issue
Block a user