diff --git a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx b/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx index c4ef3197f..32215146e 100644 --- a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx +++ b/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx @@ -1,6 +1,6 @@ import { StyleConstants } from "../../../Common/Constants"; import * as React from "react"; -import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button"; +import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button"; import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"; import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown"; import { useSubscriptions } from "../../../hooks/useSubscriptions"; @@ -39,7 +39,8 @@ const buttonStyles: IButtonStyles = { export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }> = ({ armToken }) => { const subscriptions = useSubscriptions(armToken); - const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState(); + const cachedSubscriptionId = localStorage.getItem("cachedSubscriptionId"); + const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState(cachedSubscriptionId); const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken); const [selectedAccountName, setSelectedAccoutName] = React.useState(); @@ -67,7 +68,9 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }; }), onChange: (event, option) => { - setSelectedSubscriptionId(String(option.key)); + const subscriptionId = String(option.key); + setSelectedSubscriptionId(subscriptionId); + localStorage.setItem("cachedSubscriptionId", subscriptionId); }, defaultSelectedKey: selectedSubscriptionId, placeholder: "Select subscription from list", @@ -81,7 +84,7 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }, { key: "switchAccount", - onRender: () => { + onRender: (item, dismissMenu) => { const isLoadingAccounts = false; const options = accounts.map(account => ({ @@ -102,6 +105,7 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string options, onChange: (event, option) => { setSelectedAccoutName(String(option.key)); + dismissMenu(); }, defaultSelectedKey: selectedAccountName, placeholder: placeHolderText, @@ -116,13 +120,13 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string ] }; - const buttonProps: IButtonProps = { - text: selectedAccountName || "Select Database Account", - menuProps: menuProps, - styles: buttonStyles, - className: "accountSwitchButton", - id: "accountSwitchButton" - }; - - return ; + return ( + + ); }; diff --git a/src/HostedExplorer.ts b/src/HostedExplorer.ts deleted file mode 100644 index 0d314fe27..000000000 --- a/src/HostedExplorer.ts +++ /dev/null @@ -1,1082 +0,0 @@ -import "./Shared/appInsights"; -import * as _ from "underscore"; -import * as ko from "knockout"; -import hasher from "hasher"; -import { Action } from "./Shared/Telemetry/TelemetryConstants"; -import { ArmResourceUtils } from "./Platform/Hosted/ArmResourceUtils"; -import AuthHeadersUtil from "./Platform/Hosted/Authorization"; -import { AuthType } from "./AuthType"; -import { getArcadiaAuthToken } from "./Utils/AuthorizationUtils"; -import { ActionType, PaneKind } from "./Contracts/ActionContracts"; -import * as Constants from "./Common/Constants"; -import { ControlBarComponentAdapter } from "./Explorer/Menus/NavBar/ControlBarComponentAdapter"; -import { ConsoleDataType } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { DatabaseAccount, Subscription, AccountKeys, Tenant } from "./Contracts/DataModels"; -import { - DefaultDirectoryDropdownComponent, - DefaultDirectoryDropdownProps -} from "./Explorer/Controls/Directory/DefaultDirectoryDropdownComponent"; -import { DialogComponentAdapter } from "./Explorer/Controls/DialogReactComponent/DialogComponentAdapter"; -import { DialogProps } from "./Explorer/Controls/DialogReactComponent/DialogComponent"; -import { DirectoryListProps } from "./Explorer/Controls/Directory/DirectoryListComponent"; -import { getErrorMessage } from "./Common/ErrorHandlingUtils"; -import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; -import { LocalStorageUtility, StorageKey, SessionStorageUtility } from "./Shared/StorageUtility"; -import * as Logger from "./Common/Logger"; -import { MeControlComponentProps } from "./Explorer/Menus/NavBar/MeControlComponent"; -import { MeControlComponentAdapter } from "./Explorer/Menus/NavBar/MeControlComponentAdapter"; -import { MessageTypes } from "./Contracts/ExplorerContracts"; -import * as ReactBindingHandler from "./Bindings/ReactBindingHandler"; -import { SwitchDirectoryPane, SwitchDirectoryPaneComponent } from "./Explorer/Panes/SwitchDirectoryPane"; -import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor"; -import { isInvalidParentFrameOrigin } from "./Utils/MessageValidation"; -import "../less/hostedexplorer.less"; -import "./Explorer/Menus/NavBar/MeControlComponent.less"; -import ConnectIcon from "../images/HostedConnectwhite.svg"; -import SettingsIcon from "../images/HostedSettings.svg"; -import FeedbackIcon from "../images/Feedback.svg"; -import SwitchDirectoryIcon from "../images/DirectorySwitch.svg"; -import { CommandButtonComponentProps } from "./Explorer/Controls/CommandButton/CommandButtonComponent"; - -ReactBindingHandler.Registerer.register(); -ko.components.register("switch-directory-pane", new SwitchDirectoryPaneComponent()); - -class HostedExplorer { - public navigationSelection: ko.Observable; - public isAccountActive: ko.Computed; - public controlBarComponentAdapter: ControlBarComponentAdapter; - public firewallWarningComponentAdapter: DialogComponentAdapter; - public dialogComponentAdapter: DialogComponentAdapter; - public meControlComponentAdapter: MeControlComponentAdapter; - public switchDirectoryPane: SwitchDirectoryPane; - - private _firewallWarningDialogProps: ko.Observable; - private _dialogProps: ko.Observable; - private _meControlProps: ko.Observable; - private _controlbarCommands: ko.ObservableArray; - private _directoryDropdownProps: ko.Observable; - private _directoryListProps: ko.Observable; - - constructor() { - this.navigationSelection = ko.observable("explorer"); - const updateExplorerHash = (newHash: string, oldHash: string) => this._updateExplorerWithHash(newHash); - // This pull icons from CDN, if we support standalone hosted in National Cloud, we need to change this - initializeIcons(/* optional base url */); - - this._controlbarCommands = ko.observableArray([ - { - id: "commandbutton-connect", - iconSrc: ConnectIcon, - iconAlt: "connect button", - onCommandClick: () => this.openConnectPane(), - commandButtonLabel: undefined, - ariaLabel: "connect button", - tooltipText: "Connect to a Cosmos DB account", - hasPopup: true, - disabled: false - }, - { - id: "commandbutton-settings", - iconSrc: SettingsIcon, - iconAlt: "setting button", - onCommandClick: () => this.openSettingsPane(), - commandButtonLabel: undefined, - ariaLabel: "setting button", - tooltipText: "Global settings", - hasPopup: true, - disabled: false - }, - { - id: "commandbutton-feedback", - iconSrc: FeedbackIcon, - iconAlt: "feeback button", - onCommandClick: () => - window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"), - commandButtonLabel: undefined, - ariaLabel: "feeback button", - tooltipText: "Send feedback", - hasPopup: true, - disabled: false - } - ]); - this.controlBarComponentAdapter = new ControlBarComponentAdapter(this._controlbarCommands); - - this._directoryDropdownProps = ko.observable({ - defaultDirectoryId: undefined, - directories: [], - onDefaultDirectoryChange: this._onDefaultDirectoryChange - }); - - this._directoryListProps = ko.observable({ - directories: [], - selectedDirectoryId: undefined, - onNewDirectorySelected: this._onNewDirectorySelected - }); - - this.switchDirectoryPane = new SwitchDirectoryPane(this._directoryDropdownProps, this._directoryListProps); - - this._firewallWarningDialogProps = ko.observable({ - isModal: true, - visible: false, - title: "Data Explorer Access", - subText: - 'The way Data Explorer accesses your databases and containers has changed and you need to update your Firewall settings to add your current IP address to the firewall rules. Please open Firewall blade in Azure portal, click "Add my IP address" and click ‘Save’.', - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._closeFirewallWarningDialog, - onSecondaryButtonClick: this._closeFirewallWarningDialog - }); - this.firewallWarningComponentAdapter = new DialogComponentAdapter(); - this.firewallWarningComponentAdapter.parameters = this._firewallWarningDialogProps; - - this._dialogProps = ko.observable({ - isModal: false, - visible: false, - title: undefined, - subText: undefined, - primaryButtonText: undefined, - secondaryButtonText: undefined, - onPrimaryButtonClick: undefined, - onSecondaryButtonClick: undefined - }); - this.dialogComponentAdapter = new DialogComponentAdapter(); - this.dialogComponentAdapter.parameters = this._dialogProps; - - this._meControlProps = ko.observable({ - isUserSignedIn: false, - user: { - name: undefined, - email: undefined, - tenantName: undefined, - imageUrl: undefined - }, - onSignInClick: this._onSignInClick, - onSignOutClick: this._onSignOutClick, - onSwitchDirectoryClick: this._onSwitchDirectoryClick - }); - this.meControlComponentAdapter = new MeControlComponentAdapter(); - this.meControlComponentAdapter.parameters = this._meControlProps; - - hasher.initialized.add(updateExplorerHash); - hasher.changed.add(updateExplorerHash); - hasher.init(); - window.addEventListener("message", this._handleMessage.bind(this), false); - } - - public explorer_click() { - this.navigationSelection("explorer"); - } - - public openSettingsPane(): boolean { - this._sendMessageToExplorerFrame({ - openAction: { - actionType: ActionType.OpenPane, - paneKind: PaneKind.GlobalSettings - } - }); - - return false; - } - - public openConnectPane(): boolean { - this._sendMessageToExplorerFrame({ - openAction: { - actionType: ActionType.OpenPane, - paneKind: PaneKind.AdHocAccess - } - }); - - return false; - } - - public openDirectoryPane(): void { - this.switchDirectoryPane.open(); - } - - public openAzurePortal(src: any, event: MouseEvent): boolean { - // TODO: Get environment specific azure portal url from a config file - window.open("https://portal.azure.com", "_blank"); - return false; - } - - public onOpenAzurePortalKeyPress(src: any, event: KeyboardEvent): boolean { - if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { - this.openAzurePortal(src, undefined); - return false; - } - - return true; - } - - private _handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") { - return; - } - if (typeof event.data !== "object" || !("data" in event.data)) { - return; - } - - const message: any = event.data.data; - if (message === "ready") { - this._updateExplorerWithHash(decodeURIComponent(hasher.getHash())); - } else if (message && message.type) { - this._handleMessageTypes(message); - } - } - - private _handleMessageTypes(message: any) { - switch (message.type) { - case MessageTypes.AadSignIn: - AuthHeadersUtil.signIn(); - break; - case MessageTypes.UpdateLocationHash: - if (message.locationHash) { - hasher.replaceHash(message.locationHash); - } - break; - case MessageTypes.UpdateAccountSwitch: - if (message.props) { - this._updateAccountSwitchProps(message.props); - } - if (message.click) { - this._clickAccountSwitchControl(); - } - break; - case MessageTypes.UpdateDirectoryControl: - if (message.click) { - this._clickDirectoryControl(); - } - break; - case MessageTypes.GetAccessAadRequest: - this._handleGetAccessAadRequest(); - break; - case MessageTypes.ExplorerClickEvent: - this._simulateClick(); - break; - case MessageTypes.ForbiddenError: - this._displayFirewallWarningDialog(); - break; - case MessageTypes.GetArcadiaToken: - this._getArcadiaToken(message); - } - } - - private _updateDirectoryProps( - dropdownProps?: Partial, - listProps?: Partial - ) { - if (dropdownProps) { - const propsToUpdate = this._directoryDropdownProps(); - if (dropdownProps.defaultDirectoryId) { - propsToUpdate.defaultDirectoryId = dropdownProps.defaultDirectoryId; - } - if (dropdownProps.directories) { - propsToUpdate.directories = dropdownProps.directories; - } - this._directoryDropdownProps(propsToUpdate); - } - if (listProps) { - const propsToUpdate = this._directoryListProps(); - if (listProps.selectedDirectoryId) { - propsToUpdate.selectedDirectoryId = listProps.selectedDirectoryId; - } - if (listProps.directories) { - propsToUpdate.directories = listProps.directories; - } - this._directoryListProps(propsToUpdate); - } - } - - private _updateMeControlProps(props: Partial) { - if (!props) { - return; - } - - const propsToUpdate = this._meControlProps(); - if (props.isUserSignedIn != null) { - propsToUpdate.isUserSignedIn = props.isUserSignedIn; - } - - if (props.user) { - if (props.user.name != null) { - propsToUpdate.user.name = props.user.name; - } - if (props.user.email != null) { - propsToUpdate.user.email = props.user.email; - } - if (props.user.imageUrl != null) { - propsToUpdate.user.imageUrl = props.user.imageUrl; - } - if (props.user.tenantName != null) { - propsToUpdate.user.tenantName = props.user.tenantName; - } - } - - this._meControlProps(propsToUpdate); - } - - private _updateAccountSwitchProps(props: any) { - if (!props) { - return; - } - } - - private _onAccountChange = (newAccount: DatabaseAccount) => { - if (!newAccount) { - return; - } - this._openSwitchAccountModalDialog(newAccount); - TelemetryProcessor.traceStart(Action.AccountSwitch); - }; - - private _onSubscriptionChange = (newSubscription: Subscription) => { - if (!newSubscription) { - return; - } - this._switchSubscription(newSubscription); - TelemetryProcessor.trace(Action.SubscriptionSwitch); - }; - - private _openSwitchAccountModalDialog = (newAccount: DatabaseAccount) => { - const switchAccountDialogProps: DialogProps = { - isModal: true, - visible: true, - title: `Switch account to ${newAccount.name}`, - subText: - "Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current Data Explorer tabs will be closed. Proceed anyway?", - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: () => this._onSwitchDialogOkClicked(newAccount), - onSecondaryButtonClick: this._onSwitchDialogCancelClicked - }; - this._dialogProps(switchAccountDialogProps); - }; - - private _onSwitchDialogCancelClicked = () => { - this._closeModalDialog(); - TelemetryProcessor.traceFailure(Action.AccountSwitch); - }; - - private _onSwitchDialogOkClicked = (newAccount: DatabaseAccount) => { - this._closeModalDialog(); - this._switchAccount(newAccount).then(accountResponse => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.SwitchAccount, - account: accountResponse[0], - keys: accountResponse[1], - authorizationToken: accountResponse[2] - }); - }); - TelemetryProcessor.traceSuccess(Action.AccountSwitch); - }; - - private _closeModalDialog = () => { - this._dialogProps().visible = false; - this._dialogProps.valueHasMutated(); - }; - - private _closeFirewallWarningDialog = () => { - this._firewallWarningDialogProps().visible = false; - this._firewallWarningDialogProps.valueHasMutated(); - }; - - private _displayFirewallWarningDialog = () => { - this._firewallWarningDialogProps().visible = true; - this._firewallWarningDialogProps.valueHasMutated(); - }; - - private _updateExplorerWithHash(newHash: string): void { - this._sendMessageToExplorerFrame({ - type: MessageTypes.UpdateLocationHash, - locationHash: newHash - }); - } - - private _sendMessageToExplorerFrame(data: any): void { - const explorerFrame = document.getElementById("explorerMenu") as HTMLIFrameElement; - explorerFrame && - explorerFrame.contentDocument && - explorerFrame.contentDocument.referrer && - explorerFrame.contentWindow.postMessage( - { - signature: "pcIframe", - data: data - }, - explorerFrame.contentDocument.referrer || window.location.href - ); - } - - private _onSignInClick = () => { - if (SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken)) { - SessionStorageUtility.removeEntry(StorageKey.EncryptedKeyToken); - } - const windowUrl = window.location.href; - const params = new URLSearchParams(window.parent.location.search); - if (!!params && params.has("key")) { - const keyIndex = windowUrl.indexOf("key"); - const keyLength = encodeURIComponent(params.get("key")).length; - const metaDataLength = "key=".length; - const cleanUrl = windowUrl.slice(0, keyIndex) + windowUrl.slice(keyIndex + keyLength + metaDataLength); - window.history.pushState({}, document.title, cleanUrl); - } - AuthHeadersUtil.signIn(); - TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "HostedExplorer" }); - }; - - private _onSignOutClick = () => { - AuthHeadersUtil.signOut(); - TelemetryProcessor.trace(Action.SignOutAad, undefined, { area: "HostedExplorer" }); - }; - - private _onSwitchDirectoryClick = () => { - this._clickMeControl(); - this.openDirectoryPane(); - }; - - private async _getArcadiaToken(message: any): Promise { - try { - const token = await getArcadiaAuthToken(); - this._sendMessageToExplorerFrame({ - actionType: ActionType.TransmitCachedData, - message: { - id: message && message.id, - data: JSON.stringify(token) // target expects stringified value - } - }); - } catch (error) { - const errorMessage = getErrorMessage(error); - Logger.logError(errorMessage, "HostedExplorer/_getArcadiaToken"); - this._sendMessageToExplorerFrame({ - actionType: ActionType.TransmitCachedData, - message: { - id: message && message.id, - error: errorMessage - } - }); - } - } - - private _handleGetAccessAadRequest() { - this._getAccessAad().then( - response => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.GetAccessAadResponse, - response - }); - }, - error => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.GetAccessAadResponse, - error: getErrorMessage(error) - }); - } - ); - } - - private async _getAccessAad(): Promise<[DatabaseAccount, AccountKeys, string]> { - return this._getAccessCached().catch(() => this._getAccessNew()); - } - - private async _getAccessCached(): Promise<[DatabaseAccount, AccountKeys, string]> { - if (!this._hasCachedDatabaseAccount() || !this._hasCachedTenant()) { - throw new Error("No cached account or tenant found."); - } - - const accountResourceId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); - let tenantId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - tenantId = tenantId && tenantId.indexOf("lastVisited") > -1 ? tenantId.substring("lastVisited".length) : tenantId; - - try { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Loading..." - }); - this._updateLoadingStatusText("Loading Account..."); - - const loadAccountResult = await this._loadAccount(accountResourceId, tenantId); - - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "", - selectedAccountName: loadAccountResult[0].name - }); - this._updateLoadingStatusText("Successfully loaded the account."); - - this._setAadControlBar(); - this._getTenantsHelper().then(tenants => { - this._getDefaultTenantHelper(tenants); - }); - this._getSubscriptionsHelper(tenantId, true, true).then(subs => - this._getDefaultSubscriptionHelper(subs, true, true) - ); - const subscriptionId: string = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; - this._getAccountsHelper(subscriptionId, true, true); - - return loadAccountResult; - } catch (error) { - LocalStorageUtility.removeEntry(StorageKey.DatabaseAccountId); - Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessCached"); - throw error; - } - } - - private async _loadAccount( - cosmosdbResourceId: string, - tenantId?: string - ): Promise<[DatabaseAccount, AccountKeys, string]> { - const getAccountPromise = ArmResourceUtils.getCosmosdbAccount(cosmosdbResourceId, tenantId); - const getKeysPromise = ArmResourceUtils.getCosmosdbKeys(cosmosdbResourceId, tenantId); - const getAuthToken = ArmResourceUtils.getAuthToken(tenantId); - - return Promise.all([getAccountPromise, getKeysPromise, getAuthToken]); - } - - private async _getAccessNew(): Promise<[DatabaseAccount, AccountKeys, string]> { - try { - const tenants = await this._getTenantsHelper(); - const defaultTenant = this._getDefaultTenantHelper(tenants); - - this._setAadControlBar(); - - const accountResponse = this._getAccessAfterTenantSelection(defaultTenant.tenantId); - return accountResponse; - } catch (error) { - Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessNew"); - throw error; - } - } - - private async _getAccessAfterTenantSelection(tenantId: string): Promise<[DatabaseAccount, AccountKeys, string]> { - try { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Loading..." - }); - const authToken = await ArmResourceUtils.getAuthToken(tenantId); - const subscriptions = await this._getSubscriptionsHelper(tenantId, true, true); - const defaultSubscription = this._getDefaultSubscriptionHelper(subscriptions, true, true); - - const accounts = await this._getAccountsHelper(defaultSubscription.subscriptionId, true, true); - const defaultAccount = this._getDefaultAccountHelper(accounts, true, true); - - const keys = await this._getAccountKeysHelper(defaultAccount, true); - return [defaultAccount, keys, authToken]; - } catch (error) { - Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessAfterTenantSelection"); - throw error; - } - } - - private async _getTenantsHelper( - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Promise> { - if (setLoadingStatus) { - this._updateLoadingStatusText("Loading directories..."); - } - - try { - TelemetryProcessor.traceStart(Action.FetchTenants); - const tenants = await ArmResourceUtils.listTenants(); - TelemetryProcessor.traceSuccess(Action.FetchTenants); - - if (!tenants || !tenants.length) { - if (setLoadingStatus) { - this._updateLoadingStatusText("No directories found. Please sign up for Azure."); - } - return Promise.reject(new Error("No directories found")); - } - - if (setLoadingStatus) { - this._updateLoadingStatusText("Successfully loaded directories."); - } - if (setControl) { - this._updateDirectoryProps({ directories: tenants }, { directories: tenants }); - } - return tenants; - } catch (error) { - if (setLoadingStatus) { - this._updateLoadingStatusText("Failed to load directoreis."); - } - TelemetryProcessor.traceFailure(Action.FetchTenants); - throw error; - } - } - - private _getDefaultTenantHelper( - tenants: Tenant[], - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Tenant { - if (!tenants || !tenants.length) { - return undefined; - } - - let storedDefaultTenantId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - const useLastVisitedAsDefault = - storedDefaultTenantId && storedDefaultTenantId.indexOf(DefaultDirectoryDropdownComponent.lastVisitedKey) > -1; - storedDefaultTenantId = useLastVisitedAsDefault - ? storedDefaultTenantId.substring(DefaultDirectoryDropdownComponent.lastVisitedKey.length) - : storedDefaultTenantId; - - let defaultTenant: Tenant = _.find(tenants, t => t.tenantId === storedDefaultTenantId); - if (!defaultTenant) { - defaultTenant = tenants[0]; - LocalStorageUtility.setEntryString( - StorageKey.TenantId, - `${DefaultDirectoryDropdownComponent.lastVisitedKey}${defaultTenant.tenantId}` - ); - } - - if (setControl) { - const dropdownDefaultDirectoryId = useLastVisitedAsDefault - ? DefaultDirectoryDropdownComponent.lastVisitedKey - : defaultTenant.tenantId; - - this._updateDirectoryProps( - { defaultDirectoryId: dropdownDefaultDirectoryId }, - { selectedDirectoryId: defaultTenant.tenantId } - ); - - this._updateMeControlProps({ - isUserSignedIn: true, - user: { - name: undefined, - email: undefined, - tenantName: defaultTenant && defaultTenant.displayName, - imageUrl: undefined - } - }); - } - if (setLoadingStatus) { - this._updateLoadingStatusText(`Connecting to directory: ${defaultTenant.displayName}`); - } - - return defaultTenant; - } - - private async _getSubscriptionsHelper( - tenantId?: string, - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Promise> { - if (setLoadingStatus) { - this._updateLoadingStatusText("Loading subscriptions..."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: true - }); - } - try { - TelemetryProcessor.traceStart(Action.FetchSubscriptions); - const subscriptions = await ArmResourceUtils.listSubscriptions(tenantId); - TelemetryProcessor.traceSuccess(Action.FetchSubscriptions); - - if (!subscriptions || !subscriptions.length) { - const message: string = "No Subscription Found"; - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Please - switch to a different directory with Cosmos DB accounts, or - create an subscription under this directory`, - message - ); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: false, - subscriptions: [], - accounts: [], - displayText: message - }); - } - return Promise.reject(new Error(message)); - } - if (setLoadingStatus) { - this._updateLoadingStatusText("Successfully loaded subscriptions."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: false, - subscriptions: subscriptions - }); - } - return subscriptions; - } catch (error) { - const failureMessage = "Failed to load subscriptions"; - if (setLoadingStatus) { - this._updateLoadingStatusText(failureMessage); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: false, - displayText: failureMessage - }); - } - TelemetryProcessor.traceFailure(Action.FetchSubscriptions); - throw error; - } - } - - private _getDefaultSubscriptionHelper( - subscriptions: Subscription[], - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Subscription { - if (!subscriptions || !subscriptions.length) { - return undefined; - } - - const storedAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); - const storedSubId = storedAccountId && storedAccountId.split("subscriptions/")[1].split("/")[0]; - - let defaultSub = _.find(subscriptions, s => s.subscriptionId === storedSubId); - if (!defaultSub) { - defaultSub = subscriptions[0]; - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - selectedSubscriptionId: defaultSub.subscriptionId - }); - } - if (setLoadingStatus) { - this._updateLoadingStatusText(`Connecting to subscription: ${defaultSub.displayName}`); - } - - return defaultSub; - } - - private async _getAccountsHelper( - subscriptionId: string, - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Promise> { - if (!subscriptionId) { - throw new Error("No subscription Id"); - } - - if (setLoadingStatus) { - this._updateLoadingStatusText("Loading Accounts..."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingAccounts: true, - accounts: [] - }); - } - - try { - TelemetryProcessor.traceStart(Action.FetchAccounts); - const accounts = await ArmResourceUtils.listCosmosdbAccounts([subscriptionId]); - TelemetryProcessor.traceSuccess(Action.FetchAccounts); - - if (!accounts || !accounts.length) { - const message: string = "No Account Found"; - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Please - switch to a different subscription with Cosmos DB accounts, or - - create an account in this subscription`, - message - ); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: message, - isLoadingAccounts: false, - accounts: [] - }); - } - return Promise.reject(new Error("No Account Found")); - } - if (setLoadingStatus) { - this._updateLoadingStatusText("Successfully loaded accounts."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingAccounts: false, - accounts: accounts - }); - } - return accounts; - } catch (error) { - const failureMessage = "Failed to load accounts."; - if (setLoadingStatus) { - this._updateLoadingStatusText(failureMessage); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingAccounts: false, - accounts: [], - displayText: failureMessage - }); - } - TelemetryProcessor.traceFailure(Action.FetchAccounts); - throw error; - } - } - - private _getDefaultAccountHelper( - accounts: DatabaseAccount[], - setControl: boolean = true, - setLoadingStatus: boolean = true - ): DatabaseAccount { - if (!accounts || !accounts.length) { - return undefined; - } - - let storedDefaultAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); - let defaultAccount = _.find(accounts, a => a.id === storedDefaultAccountId); - - if (!defaultAccount) { - defaultAccount = accounts[0]; - LocalStorageUtility.setEntryString(StorageKey.DatabaseAccountId, defaultAccount.id); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "", - selectedAccountName: defaultAccount.name - }); - } - if (setLoadingStatus) { - this._updateLoadingStatusText(`Connecting to Azure Cosmos DB account: ${defaultAccount.name}`); - } - - return defaultAccount; - } - - private async _getAccountKeysHelper( - account: DatabaseAccount, - setLoadingStatus: boolean = true - ): Promise { - try { - if (setLoadingStatus) { - this._updateLoadingStatusText(`Getting authentication token for Azure Cosmos DB account: ${account.name}`); - } - - TelemetryProcessor.traceStart(Action.GetAccountKeys); - const keys = await ArmResourceUtils.getCosmosdbKeys(account.id); - TelemetryProcessor.traceSuccess(Action.GetAccountKeys); - - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Successfully got authentication token for Azure Cosmos DB account: ${account.name}` - ); - } - return keys; - } catch (error) { - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Failed to get authentication token for Azure Cosmos DB account: ${account.name}` - ); - } - TelemetryProcessor.traceFailure(Action.GetAccountKeys); - throw error; - } - } - - private _switchSubscription = async (newSubscription: Subscription): Promise> => { - if (!newSubscription) { - throw new Error("no subscription specified"); - } - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - selectedSubscriptionId: newSubscription.subscriptionId - }); - const id: string = _.uniqueId(); - this._logConsoleMessage( - ConsoleDataType.InProgress, - `Getting Cosmos DB accounts from subscription: ${newSubscription.displayName}`, - id - ); - - try { - const accounts = await this._getAccountsHelper(newSubscription.subscriptionId, true); - - this._logConsoleMessage(ConsoleDataType.Info, "Successfully fetched Cosmos DB accounts."); - this._clearInProgressMessageWithId(id); - - return accounts; - } catch (error) { - this._logConsoleMessage(ConsoleDataType.Error, `Failed to fetch accounts: ${getErrorMessage(error)}`); - this._clearInProgressMessageWithId(id); - - throw error; - } - }; - - private _switchAccount = async (newAccount: DatabaseAccount): Promise<[DatabaseAccount, AccountKeys, string]> => { - if (!newAccount) { - throw new Error("No account passed in"); - } - - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Loading..." - }); - const id: string = _.uniqueId(); - this._logConsoleMessage(ConsoleDataType.InProgress, `Connecting to Cosmos DB account: ${newAccount.name}`, id); - - try { - const loadAccountResponse = await this._loadAccount(newAccount.id); - const account = loadAccountResponse[0]; - - LocalStorageUtility.setEntryString(StorageKey.DatabaseAccountId, account.id); - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "", - selectedAccountName: account.name - }); - this._logConsoleMessage(ConsoleDataType.Info, "Connection successful"); - this._clearInProgressMessageWithId(id); - - return loadAccountResponse; - } catch (error) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Error loading account" - }); - this._updateLoadingStatusText(`Failed to load selected account: ${newAccount.name}`); - this._logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${getErrorMessage(error)}`); - this._clearInProgressMessageWithId(id); - throw error; - } - }; - - private _hasCachedDatabaseAccount(): boolean { - return LocalStorageUtility.hasItem(StorageKey.DatabaseAccountId); - } - - private _hasCachedTenant(): boolean { - return LocalStorageUtility.hasItem(StorageKey.TenantId); - } - - private _logConsoleMessage(consoleDataType: ConsoleDataType, message: string, id?: string) { - this._sendMessageToExplorerFrame({ - type: MessageTypes.SendNotification, - consoleDataType, - message, - id: id || undefined - }); - } - - private _clearInProgressMessageWithId(id: string) { - this._sendMessageToExplorerFrame({ - type: MessageTypes.ClearNotification, - id - }); - } - - private _updateLoadingStatusText(text: string, title?: string) { - this._sendMessageToExplorerFrame({ - type: MessageTypes.LoadingStatus, - text, - title - }); - } - - private _setAadControlBar() { - const switchDirectoryCommand: CommandButtonComponentProps = { - iconSrc: SwitchDirectoryIcon, - iconAlt: "switch directory button", - onCommandClick: () => this.openDirectoryPane(), - commandButtonLabel: undefined, - ariaLabel: "switch directory button", - tooltipText: "Switch Directory", - hasPopup: true, - disabled: false, - id: "directorySwitchButton" - }; - - this._controlbarCommands.splice(0, 1, switchDirectoryCommand); - } - - private _onDefaultDirectoryChange = (newDirectory: Tenant) => { - this._updateDirectoryProps({ defaultDirectoryId: newDirectory.tenantId }); - if (newDirectory.tenantId === DefaultDirectoryDropdownComponent.lastVisitedKey) { - const storedDirectoryId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - LocalStorageUtility.setEntryString( - StorageKey.TenantId, - `${DefaultDirectoryDropdownComponent.lastVisitedKey}${storedDirectoryId}` - ); - return; - } - LocalStorageUtility.setEntryString(StorageKey.TenantId, newDirectory.tenantId); - TelemetryProcessor.trace(Action.DefaultTenantSwitch); - }; - - private _onNewDirectorySelected = (newDirectory: Tenant) => { - this.switchDirectoryPane.close(); - this._updateDirectoryProps(null, { selectedDirectoryId: newDirectory.tenantId }); - this._updateCacheOnNewDirectorySelected(newDirectory); - this._updateMeControlProps({ - user: { tenantName: newDirectory.displayName, name: undefined, email: undefined, imageUrl: undefined } - }); - this._getAccessAfterTenantSelection(newDirectory.tenantId).then( - accountResponse => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.SwitchAccount, - account: accountResponse[0], - keys: accountResponse[1], - authorizationToken: accountResponse[2] - }); - }, - error => { - Logger.logError(getErrorMessage(error), "HostedExplorer/_onNewDirectorySelected"); - } - ); - TelemetryProcessor.trace(Action.TenantSwitch); - }; - - private _updateCacheOnNewDirectorySelected(newDirectory: Tenant) { - const storedDefaultTenantId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - if (storedDefaultTenantId.indexOf(DefaultDirectoryDropdownComponent.lastVisitedKey) >= 0) { - LocalStorageUtility.setEntryString( - StorageKey.TenantId, - `${DefaultDirectoryDropdownComponent.lastVisitedKey}${newDirectory.tenantId}` - ); - } - LocalStorageUtility.removeEntry(StorageKey.DatabaseAccountId); - } - - private _clickDirectoryControl() { - document.getElementById("directorySwitchButton").click(); - } - - private _clickAccountSwitchControl() { - document.getElementById("accountSwitchButton").click(); - } - - private _clickMeControl() { - document.getElementById("mecontrolHeader").click(); - } - - /** - * The iframe swallows any click event which breaks the logic to dismiss the menu, so we simulate a click from the message - */ - private _simulateClick() { - const event = document.createEvent("Events"); - event.initEvent("click", true, false); - document.getElementsByTagName("header")[0].dispatchEvent(event); - } -} - -const hostedExplorer = new HostedExplorer(); -ko.applyBindings(hostedExplorer); diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 774093f31..7aa713f3e 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -33,6 +33,9 @@ import { AuthType } from "./AuthType"; initializeIcons(); const msal = new Msal.UserAgentApplication({ + cache: { + cacheLocation: "localStorage" + }, auth: { authority: "https://login.microsoft.com/common", clientId: "203f1145-856a-4232-83d4-a43568fba23d", @@ -40,6 +43,9 @@ const msal = new Msal.UserAgentApplication({ } }); +const cachedAccount = msal.getAllAccounts()?.[0]; +const cachedTenantId = localStorage.getItem("cachedTenantId"); + const App: React.FunctionComponent = () => { // Hooks for handling encrypted portal tokens const params = new URLSearchParams(window.location.search); @@ -51,11 +57,13 @@ const App: React.FunctionComponent = () => { const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false); // Hooks for AAD authentication - const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(false); - const [account, setAccount] = React.useState(); + const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean( + Boolean(cachedAccount && cachedTenantId) || false + ); + const [account, setAccount] = React.useState(cachedAccount); + const [tenantId, setTenantId] = React.useState(cachedTenantId); const [graphToken, setGraphToken] = React.useState(); const [armToken, setArmToken] = React.useState(); - const [tenantId, setTenantId] = React.useState(); const [connectionString, setConnectionString] = React.useState(""); const login = React.useCallback(async () => { @@ -63,17 +71,17 @@ const App: React.FunctionComponent = () => { setLoggedIn(); setAccount(response.account); setTenantId(response.tenantId); + localStorage.setItem("cachedTenantId", response.tenantId); }, []); const logout = React.useCallback(() => { - msal.logout(); setLoggedOut(); + localStorage.removeItem("cachedTenantId"); + msal.logout(); }, []); React.useEffect(() => { if (account && tenantId) { - console.log(msal.authority); - console.log("Getting tokens for", tenantId); Promise.all([ msal.acquireTokenSilent({ scopes: ["https://graph.windows.net//.default"] @@ -227,7 +235,6 @@ const App: React.FunctionComponent = () => { id="connectWithConnectionString" onSubmit={async event => { event.preventDefault(); - // const foo = parseConnectionString(connectionString); const headers = new Headers(); headers.append(HttpHeaders.connectionString, connectionString); const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken"; @@ -239,7 +246,6 @@ const App: React.FunctionComponent = () => { const result: GenerateTokenResponse = JSON.parse(await response.json()); console.log(result.readWrite || result.read); setEncryptedToken(decodeURIComponent(result.readWrite || result.read)); - event.preventDefault(); }} >

@@ -315,4 +321,4 @@ const App: React.FunctionComponent = () => { ); }; -render(, document.body); +render(, document.getElementById("App")); diff --git a/src/Main.tsx b/src/Main.tsx index 7bc28ec60..a63f4ed38 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -55,9 +55,7 @@ import "url-polyfill/url-polyfill.min"; initializeIcons(); -import * as Emulator from "./Platform/Emulator/Main"; import Hosted from "./Platform/Hosted/Main"; -import * as Portal from "./Platform/Portal/Main"; import { AuthType } from "./AuthType"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; @@ -72,10 +70,28 @@ import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import refreshImg from "../images/refresh-cosmos.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg"; import { KOCommentEnd, KOCommentIfStart } from "./koComment"; +import { AccountKind, DefaultAccountExperience, TagNames } from "./Common/Constants"; // TODO: Encapsulate and reuse all global variables as environment variables window.authType = AuthType.AAD; +const emulatorAccount = { + name: "", + id: "", + location: "", + type: "", + kind: AccountKind.DocumentDB, + tags: { + [TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB + }, + properties: { + documentEndpoint: "", + tableEndpoint: "", + gremlinEndpoint: "", + cassandraEndpoint: "" + } +}; + const App: React.FunctionComponent = () => { useEffect(() => { initializeConfiguration().then(config => { @@ -84,9 +100,25 @@ const App: React.FunctionComponent = () => { explorer = Hosted.initializeExplorer(); } else if (config.platform === Platform.Emulator) { window.authType = AuthType.MasterKey; - explorer = Emulator.initializeExplorer(); + const explorer = new Explorer(); + explorer.databaseAccount(emulatorAccount); + explorer.isAccountReady(true); } else if (config.platform === Platform.Portal) { - explorer = Portal.initializeExplorer(); + explorer = new Explorer(); + + // In development mode, try to load the iframe message from session storage. + // This allows webpack hot reload to funciton properly + if (process.env.NODE_ENV === "development") { + const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); + if (initMessage) { + const message = JSON.parse(initMessage); + console.warn("Loaded cached portal iframe message from session storage"); + console.dir(message); + explorer.initDataExplorerWithFrameInputs(message); + } + } + + window.addEventListener("message", explorer.handleMessage.bind(explorer), false); } applyExplorerBindings(explorer); }); diff --git a/src/Platform/Emulator/Main.ts b/src/Platform/Emulator/Main.ts deleted file mode 100644 index d3f5783a1..000000000 --- a/src/Platform/Emulator/Main.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Explorer from "../../Explorer/Explorer"; -import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants"; - -export function initializeExplorer(): Explorer { - const explorer = new Explorer(); - explorer.databaseAccount({ - name: "", - id: "", - location: "", - type: "", - kind: AccountKind.DocumentDB, - tags: { - [TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB - }, - properties: { - documentEndpoint: "", - tableEndpoint: "", - gremlinEndpoint: "", - cassandraEndpoint: "" - } - }); - - explorer.isAccountReady(true); - return explorer; -} diff --git a/src/Platform/Hosted/ArmResourceUtils.ts b/src/Platform/Hosted/ArmResourceUtils.ts deleted file mode 100644 index ad39ffb8e..000000000 --- a/src/Platform/Hosted/ArmResourceUtils.ts +++ /dev/null @@ -1,180 +0,0 @@ -import AuthHeadersUtil from "./Authorization"; -import * as Constants from "../../Common/Constants"; -import * as Logger from "../../Common/Logger"; -import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels"; -import { configContext } from "../../ConfigContext"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; - -// TODO: 421864 - add a fetch wrapper -export class ArmResourceUtils { - private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT; - private static readonly _armApiVersion: string = configContext.ARM_API_VERSION; - private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA; - - // TODO: 422867 - return continuation token instead of read through - public static async listTenants(): Promise> { - let tenants: Array = []; - - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea); - let nextLink = `${ArmResourceUtils._armEndpoint}/tenants?api-version=2017-08-01`; - - while (nextLink) { - const response: Response = await fetch(nextLink, { headers: fetchHeaders }); - const result: TenantListResult = - response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - nextLink = result.nextLink; - tenants = [...tenants, ...result.value]; - } - return tenants; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/listTenants"); - throw error; - } - } - - // TODO: 422867 - return continuation token instead of read through - public static async listSubscriptions(tenantId?: string): Promise> { - let subscriptions: Array = []; - - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - let nextLink = `${ArmResourceUtils._armEndpoint}/subscriptions?api-version=${ArmResourceUtils._armApiVersion}`; - - while (nextLink) { - const response: Response = await fetch(nextLink, { headers: fetchHeaders }); - const result: SubscriptionListResult = - response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - nextLink = result.nextLink; - const validSubscriptions = result.value.filter( - sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue" - ); - subscriptions = [...subscriptions, ...validSubscriptions]; - } - return subscriptions; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/listSubscriptions"); - throw error; - } - } - - // TODO: 422867 - return continuation token instead of read through - public static async listCosmosdbAccounts( - subscriptionIds: string[], - tenantId?: string - ): Promise> { - if (!subscriptionIds || !subscriptionIds.length) { - return Promise.reject("No subscription passed in"); - } - - let accounts: Array = []; - - try { - const subscriptionFilter = "subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'"; - const urlFilter = `$filter=(${subscriptionFilter}) and (resourceType eq 'microsoft.documentdb/databaseaccounts')`; - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - let nextLink = `${ArmResourceUtils._armEndpoint}/resources?api-version=${ArmResourceUtils._armApiVersion}&${urlFilter}`; - - while (nextLink) { - const response: Response = await fetch(nextLink, { headers: fetchHeaders }); - const result: AccountListResult = - response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - nextLink = result.nextLink; - accounts = [...accounts, ...result.value]; - } - return accounts; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/listAccounts"); - throw error; - } - } - - public static async getCosmosdbAccount(cosmosdbResourceId: string, tenantId?: string): Promise { - if (!cosmosdbResourceId) { - return Promise.reject("No Cosmos DB resource id passed in"); - } - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - const url = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}?api-version=${Constants.ArmApiVersions.documentDB}`; - - const response: Response = await fetch(url, { headers: fetchHeaders }); - const result: DatabaseAccount = response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - return result; - } catch (error) { - throw error; - } - } - - public static async getCosmosdbKeys(cosmosdbResourceId: string, tenantId?: string): Promise { - if (!cosmosdbResourceId) { - return Promise.reject("No Cosmos DB resource id passed in"); - } - - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - const readWriteKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/listKeys?api-version=${Constants.ArmApiVersions.documentDB}`; - const readOnlyKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/readOnlyKeys?api-version=${Constants.ArmApiVersions.documentDB}`; - let response: Response = await fetch(readWriteKeysUrl, { headers: fetchHeaders, method: "POST" }); - if (response.status === Constants.HttpStatusCodes.Forbidden) { - // fetch read only keys for readers - response = await fetch(readOnlyKeysUrl, { headers: fetchHeaders, method: "POST" }); - } - const result: AccountKeys = - response.status === Constants.HttpStatusCodes.NoContent || - response.status === Constants.HttpStatusCodes.NotModified - ? null - : await response.json(); - if (!response.ok) { - throw result; - } - return result; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAccountKeys"); - throw error; - } - } - - public static async getAuthToken(tenantId?: string): Promise { - try { - const token = await AuthHeadersUtil.getAccessToken(ArmResourceUtils._armAuthArea, tenantId); - return token; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAuthToken"); - throw error; - } - } - - private static async _getAuthHeader(authArea: string, tenantId?: string): Promise { - const token = await AuthHeadersUtil.getAccessToken(authArea, tenantId); - let fetchHeaders = new Headers(); - fetchHeaders.append("authorization", `Bearer ${token}`); - return fetchHeaders; - } -} - -interface TenantListResult { - nextLink: string; - value: Tenant[]; -} - -interface SubscriptionListResult { - nextLink: string; - value: Subscription[]; -} - -interface AccountListResult { - nextLink: string; - value: DatabaseAccount[]; -} diff --git a/src/Platform/Hosted/Authorization.ts b/src/Platform/Hosted/Authorization.ts index 8941a03e9..cec42cecc 100644 --- a/src/Platform/Hosted/Authorization.ts +++ b/src/Platform/Hosted/Authorization.ts @@ -12,16 +12,6 @@ import { userContext } from "../../UserContext"; export default class AuthHeadersUtil { public static serverId: string = Constants.ServerIds.productionPortal; - private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d"; - private static readonly _aadEndpoint: string = configContext.AAD_ENDPOINT; - private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT; - private static readonly _arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT; - private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA; - private static readonly _graphEndpoint: string = configContext.GRAPH_ENDPOINT; - private static readonly _graphApiVersion: string = configContext.GRAPH_API_VERSION; - - private static _authContext: any = {}; - public static getAccessInputMetadata(accessInput: string): Q.Promise { const deferred: Q.Deferred = Q.defer(); const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`; @@ -103,146 +93,6 @@ export default class AuthHeadersUtil { }); } - public static isUserSignedIn(): boolean { - const user = AuthHeadersUtil._authContext.getCachedUser(); - return !!user; - } - public static signIn() { - if (!AuthHeadersUtil.isUserSignedIn()) { - AuthHeadersUtil._authContext.login(); - } - } - - public static signOut() { - AuthHeadersUtil._authContext.logOut(); - } - - /** - * Process token from oauth after login or get cached - */ - public static processTokenResponse() { - const isCallback = AuthHeadersUtil._authContext.isCallback(window.location.hash); - if (isCallback && !AuthHeadersUtil._authContext.getLoginError()) { - AuthHeadersUtil._authContext.handleWindowCallback(); - } - } - - /** - * Get auth token to access apis (Graph, ARM) - * - * @param authEndpoint Default to ARM endpoint - * @param tenantId if tenant id provided, tenant id will set at global. Can be reset with 'common' - */ - public static async getAccessToken( - authEndpoint: string = AuthHeadersUtil._armAuthArea, - tenantId?: string - ): Promise { - const AuthorizationType: string = (window).authType; - if (AuthorizationType === AuthType.EncryptedToken) { - // setting authorization header to an undefined value causes the browser to exclude - // the header, which is expected here - throw new Error("auth type is encrypted token, should not get access token"); - } - - return new Promise(async (resolve, reject) => { - if (tenantId) { - // if tenant id passed in, we will use this tenant id for all the rest calls until next tenant id passed in - AuthHeadersUtil._authContext.config.tenant = tenantId; - } - - AuthHeadersUtil._authContext.acquireToken( - authEndpoint, - AuthHeadersUtil._authContext.config.tenant, - (errorResponse: any, token: any) => { - if (errorResponse && typeof errorResponse === "string") { - if (errorResponse.indexOf("login is required") >= 0 || errorResponse.indexOf("AADSTS50058") === 0) { - // Handle error AADSTS50058: A silent sign-in request was sent but no user is signed in. - // The user's cached token is invalid, hence we let the user login again. - AuthHeadersUtil._authContext.login(); - return; - } - if ( - this._isMultifactorAuthRequired(errorResponse) || - errorResponse.indexOf("AADSTS53000") > -1 || - errorResponse.indexOf("AADSTS65001") > -1 - ) { - // Handle error AADSTS50079 and AADSTS50076: User needs to use multifactor authentication and acquireToken fails silent. Redirect - // Handle error AADSTS53000: User needs to use compliant device to access resource when Conditional Access Policy is set up for user. - AuthHeadersUtil._authContext.acquireTokenRedirect( - authEndpoint, - AuthHeadersUtil._authContext.config.tenant - ); - return; - } - } - if (errorResponse || !token) { - Logger.logError(errorResponse, "Hosted/Authorization/_getAuthHeader"); - reject(errorResponse); - return; - } - resolve(token); - } - ); - }); - } - - public static async getPhotoFromGraphAPI(): Promise { - const token = await this.getAccessToken(AuthHeadersUtil._graphEndpoint); - const headers = new Headers(); - headers.append("Authorization", `Bearer ${token}`); - - try { - const response: Response = await fetch( - `${AuthHeadersUtil._graphEndpoint}/me/thumbnailPhoto?api-version=${AuthHeadersUtil._graphApiVersion}`, - { - method: "GET", - headers: headers - } - ); - if (!response.ok) { - throw response; - } - return response.blob(); - } catch (err) { - return new Blob(); - } - } - - private static async _getTenant(subId: string): Promise { - if (subId) { - try { - // Follow https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/azure-resource-manager/resource-manager-api-authentication.md - // TenantId will be returned in the header of the response. - const response: Response = await fetch( - `https://management.core.windows.net/subscriptions/${subId}?api-version=2015-01-01` - ); - if (!response.ok) { - throw response; - } - } catch (reason) { - if (reason.status === 401) { - const authUrl: string = reason.headers - .get("www-authenticate") - .split(",")[0] - .split("=")[1]; - // Fetch the tenant GUID ID and the length should be 36. - const tenantId: string = authUrl.substring(authUrl.lastIndexOf("/") + 1, authUrl.lastIndexOf("/") + 37); - return Promise.resolve(tenantId); - } - } - } - return Promise.resolve(undefined); - } - - private static _isMultifactorAuthRequired(errorResponse: string): boolean { - for (const code of ["AADSTS50079", "AADSTS50076"]) { - if (errorResponse.indexOf(code) === 0) { - return true; - } - } - return false; - } - private static _generateResourceUrl(): string { const databaseAccount = userContext.databaseAccount; const subscriptionId: string = userContext.subscriptionId; diff --git a/src/Platform/Hosted/Main.ts b/src/Platform/Hosted/Main.ts index b1c6859fa..a23568f6b 100644 --- a/src/Platform/Hosted/Main.ts +++ b/src/Platform/Hosted/Main.ts @@ -3,7 +3,6 @@ import AuthHeadersUtil from "./Authorization"; import Q from "q"; import { AccessInputMetadata, - AccountKeys, ApiKind, DatabaseAccount, GenerateTokenResponse, @@ -11,12 +10,10 @@ import { } from "../../Contracts/DataModels"; import { AuthType } from "../../AuthType"; import { CollectionCreation } from "../../Shared/Constants"; -import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; import { DataExplorerInputsFrame } from "../../Contracts/ViewModels"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { HostedUtils } from "./HostedUtils"; import { sendMessage } from "../../Common/MessageHandler"; -import { MessageTypes } from "../../Contracts/ExplorerContracts"; import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { SubscriptionUtilMappings } from "../../Shared/Constants"; import "../../Explorer/Tables/DataTable/DataTableBindingManager"; @@ -24,35 +21,17 @@ import Explorer from "../../Explorer/Explorer"; import { updateUserContext } from "../../UserContext"; import { configContext } from "../../ConfigContext"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; +import { extractFeatures } from "./extractFeatures"; export default class Main { private static _databaseAccountId: string; private static _encryptedToken: string; private static _accessInputMetadata: AccessInputMetadata; - private static _features: { [key: string]: string }; - // For AAD, Need to post message to hosted frame to do the auth - // Use local deferred variable as work around until we find better solution - private static _getAadAccessDeferred: Q.Deferred; - private static _explorer: Explorer; - - public static isUsingEncryptionToken(): boolean { - const params = new URLSearchParams(window.parent.location.search); - if ((!!params && params.has("key")) || Main._hasCachedEncryptedKey()) { - return true; - } - return false; - } public static initializeExplorer(): Explorer { - window.addEventListener("message", this._handleMessage.bind(this), false); - this._features = {}; const params = new URLSearchParams(window.location.search); let authType: string = params && params.get("authType"); - if (params) { - this._features = Main.extractFeatures(params); - } - // Encrypted token flow if (params && params.has("key")) { Main._encryptedToken = encodeURIComponent(params.get("key")); @@ -60,39 +39,22 @@ export default class Main { authType = AuthType.EncryptedToken; } - (window).authType = authType; - if (!authType) { - throw new Error("Sign in needed"); - } - - const explorer: Explorer = this._instantiateExplorer(); + const explorer = new Explorer(); + // workaround to resolve cyclic refs with view // TODO. Is this even needed anymore? + explorer.renewExplorerShareAccess = Main.renewExplorerAccess; + window.addEventListener("message", explorer.handleMessage.bind(explorer), false); if (authType === AuthType.EncryptedToken) { updateUserContext({ accessToken: Main._encryptedToken }); Main._initDataExplorerFrameInputs(explorer); } else if (authType === AuthType.AAD) { - this._explorer = explorer; } else { Main._initDataExplorerFrameInputs(explorer); } return explorer; } - public static extractFeatures(params: URLSearchParams): { [key: string]: string } { - const featureParamRegex = /feature.(.*)/i; - const features: { [key: string]: string } = {}; - params.forEach((value: string, param: string) => { - if (featureParamRegex.test(param)) { - const matches: string[] = param.match(featureParamRegex); - if (matches.length > 0) { - features[matches[1].toLowerCase()] = value; - } - } - }); - return features; - } - public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties { let accountEndpoint: string; let collectionId: string; @@ -193,16 +155,6 @@ export default class Main { return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); }; - public static getUninitializedExplorerForGuestAccess(): Explorer { - const explorer = Main._instantiateExplorer(); - if (window.authType === AuthType.AAD) { - this._explorer = explorer; - } - (window).dataExplorer = explorer; - - return explorer; - } - private static _initDataExplorerFrameInputs( explorer: Explorer, masterKey?: string /* master key extracted from connection string if available */, @@ -230,13 +182,6 @@ export default class Main { const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( Main._accessInputMetadata.apiKind ); - sendMessage({ - type: MessageTypes.UpdateAccountSwitch, - props: { - authType: AuthType.EncryptedToken, - selectedAccountName: Main._accessInputMetadata.accountName - } - }); return explorer.initDataExplorerWithFrameInputs({ databaseAccount: { id: Main._databaseAccountId, @@ -250,7 +195,7 @@ export default class Main { masterKey, hasWriteAccess: true, // TODO: we should embed this information in the token ideally authorizationToken: undefined, - features: this._features, + features: extractFeatures(), csmEndpoint: undefined, dnsSuffix: null, serverId: serverId, @@ -270,7 +215,7 @@ export default class Main { masterKey, hasWriteAccess: true, //TODO: 425017 - support read access authorizationToken, - features: this._features, + features: extractFeatures(), csmEndpoint: undefined, dnsSuffix: null, serverId: serverId, @@ -300,7 +245,7 @@ export default class Main { masterKey, hasWriteAccess: true, // TODO: we should embed this information in the token ideally authorizationToken: undefined, - features: this._features, + features: extractFeatures(), csmEndpoint: undefined, dnsSuffix: null, serverId: serverId, @@ -316,32 +261,6 @@ export default class Main { throw new Error(`Unsupported AuthType ${authType}`); } - private static _instantiateExplorer(): Explorer { - const explorer = new Explorer(); - // workaround to resolve cyclic refs with view - explorer.renewExplorerShareAccess = Main.renewExplorerAccess; - window.addEventListener("message", explorer.handleMessage.bind(explorer), false); - - // Hosted needs click to dismiss any menu - if (window.authType === AuthType.AAD) { - window.addEventListener( - "click", - () => { - sendMessage({ - type: MessageTypes.ExplorerClickEvent - }); - }, - true - ); - } - - return explorer; - } - - private static _hasCachedEncryptedKey(): boolean { - return SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken); - } - private static _getDatabaseAccountKindFromExperience(apiExperience: string): string { if (apiExperience === Constants.DefaultAccountExperience.MongoDB) { return Constants.AccountKind.MongoDB; @@ -354,19 +273,9 @@ export default class Main { return Constants.AccountKind.GlobalDocumentDB; } - private static _getAccessInputMetadata(accessInput: string): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - AuthHeadersUtil.getAccessInputMetadata(accessInput).then( - (metadata: any) => { - Main._accessInputMetadata = metadata; - deferred.resolve(); - }, - (error: any) => { - deferred.reject(error); - } - ); - - return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); + private static async _getAccessInputMetadata(accessInput: string): Promise { + const metadata = await AuthHeadersUtil.getAccessInputMetadata(accessInput); + Main._accessInputMetadata = metadata; } private static _getMasterKeyFromConnectionString(connectionString: string): string { @@ -447,84 +356,4 @@ export default class Main { explorer.isAccountReady.valueHasMutated(); sendMessage("ready"); } - - private static _shouldProcessMessage(event: MessageEvent): boolean { - if (typeof event.data !== "object") { - return false; - } - if (event.data["signature"] !== "pcIframe") { - return false; - } - if (!("data" in event.data)) { - return false; - } - if (typeof event.data["data"] !== "object") { - return false; - } - return true; - } - - private static _handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (!this._shouldProcessMessage(event)) { - return; - } - - const message: any = event.data.data; - if (message.type) { - if (message.type === MessageTypes.GetAccessAadResponse && (message.response || message.error)) { - if (message.response) { - Main._handleGetAccessAadSucceed(message.response); - } - if (message.error) { - Main._handleGetAccessAadFailed(message.error); - } - return; - } - if (message.type === MessageTypes.SwitchAccount && message.account && message.keys) { - Main._handleSwitchAccountSucceed(message.account, message.keys, message.authorizationToken); - return; - } - } - } - - private static _handleSwitchAccountSucceed(account: DatabaseAccount, keys: AccountKeys, authorizationToken: string) { - if (!this._explorer) { - console.error("no explorer found"); - return; - } - - this._explorer.hideConnectExplorerForm(); - - const masterKey = Main._getMasterKey(keys); - this._explorer.notificationConsoleData([]); - Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken); - } - - private static _handleGetAccessAadSucceed(response: [DatabaseAccount, AccountKeys, string]) { - if (!response || response.length < 1) { - return; - } - const account = response[0]; - const masterKey = Main._getMasterKey(response[1]); - const authorizationToken = response[2]; - Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken); - this._getAadAccessDeferred.resolve(this._explorer); - } - - private static _getMasterKey(keys: AccountKeys): string { - return ( - keys?.primaryMasterKey ?? - keys?.secondaryMasterKey ?? - keys?.primaryReadonlyMasterKey ?? - keys?.secondaryReadonlyMasterKey - ); - } - - private static _handleGetAccessAadFailed(error: any) { - this._getAadAccessDeferred.reject(error); - } } diff --git a/src/Platform/Hosted/Maint.test.ts b/src/Platform/Hosted/Maint.test.ts index 4c520cb78..28ca1d7df 100644 --- a/src/Platform/Hosted/Maint.test.ts +++ b/src/Platform/Hosted/Maint.test.ts @@ -1,20 +1,6 @@ import Main from "./Main"; describe("Main", () => { - it("correctly detects feature flags", () => { - // Search containing non-features, with Camelcase keys and uri encoded values - const params = new URLSearchParams( - "?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey" - ); - const features = Main.extractFeatures(params); - - expect(features).toEqual({ - notebookserverurl: "https://localhost:10001/12345/notebook", - notebookservertoken: "token", - enablenotebooks: "true" - }); - }); - it("correctly parses resource token connection string", () => { const connectionString = "AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"; diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts new file mode 100644 index 000000000..3567134d2 --- /dev/null +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -0,0 +1,17 @@ +import { extractFeatures } from "./extractFeatures"; + +describe("extractFeatures", () => { + it("correctly detects feature flags", () => { + // Search containing non-features, with Camelcase keys and uri encoded values + const params = new URLSearchParams( + "?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey" + ); + const features = extractFeatures(params); + + expect(features).toEqual({ + notebookserverurl: "https://localhost:10001/12345/notebook", + notebookservertoken: "token", + enablenotebooks: "true" + }); + }); +}); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts new file mode 100644 index 000000000..f5138d392 --- /dev/null +++ b/src/Platform/Hosted/extractFeatures.ts @@ -0,0 +1,16 @@ +const parentParams = new URLSearchParams(window.parent.location.search); + +export function extractFeatures(params?: URLSearchParams): { [key: string]: string } { + params = params || parentParams; + const featureParamRegex = /feature.(.*)/i; + const features: { [key: string]: string } = {}; + params.forEach((value: string, param: string) => { + if (featureParamRegex.test(param)) { + const matches: string[] = param.match(featureParamRegex); + if (matches.length > 0) { + features[matches[1].toLowerCase()] = value; + } + } + }); + return features; +} diff --git a/src/Platform/Portal/Main.ts b/src/Platform/Portal/Main.ts deleted file mode 100644 index 5d0b652ab..000000000 --- a/src/Platform/Portal/Main.ts +++ /dev/null @@ -1,23 +0,0 @@ -import "../../Explorer/Tables/DataTable/DataTableBindingManager"; -import Explorer from "../../Explorer/Explorer"; -import { handleMessage } from "../../Controls/Heatmap/Heatmap"; - -export function initializeExplorer(): Explorer { - const explorer = new Explorer(); - - // In development mode, try to load the iframe message from session storage. - // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { - const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); - if (initMessage) { - const message = JSON.parse(initMessage); - console.warn("Loaded cached portal iframe message from session storage"); - console.dir(message); - explorer.initDataExplorerWithFrameInputs(message); - } - } - - window.addEventListener("message", explorer.handleMessage.bind(explorer), false); - - return explorer; -} diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 48f023dc9..eda4d766d 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -1,11 +1,9 @@ -import * as Constants from "../Common/Constants"; -import * as ViewModels from "../Contracts/ViewModels"; -import AuthHeadersUtil from "../Platform/Hosted/Authorization"; import { AuthType } from "../AuthType"; +import * as Constants from "../Common/Constants"; import * as Logger from "../Common/Logger"; import { configContext, Platform } from "../ConfigContext"; +import * as ViewModels from "../Contracts/ViewModels"; import { userContext } from "../UserContext"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { if (window.authType === AuthType.EncryptedToken) { @@ -21,19 +19,6 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet } } -export async function getArcadiaAuthToken( - arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT, - tenantId?: string -): Promise { - try { - const token = await AuthHeadersUtil.getAccessToken(arcadiaEndpoint, tenantId); - return token; - } catch (error) { - Logger.logError(getErrorMessage(error), "AuthorizationUtils/getArcadiaAuthToken"); - throw error; - } -} - export function decryptJWTToken(token: string) { if (!token) { Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken"); diff --git a/src/hostedExplorer.html b/src/hostedExplorer.html index dc3e3a42c..1c46ca907 100644 --- a/src/hostedExplorer.html +++ b/src/hostedExplorer.html @@ -7,5 +7,6 @@ +