import * as Constants from "../../Common/Constants"; import AuthHeadersUtil from "./Authorization"; import Q from "q"; import { AccessInputMetadata, AccountKeys, ApiKind, DatabaseAccount, GenerateTokenResponse, resourceTokenConnectionStringProperties } 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"; import Explorer from "../../Explorer/Explorer"; import { updateUserContext } from "../../UserContext"; import { configContext } from "../../ConfigContext"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; 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(): Q.Promise { window.addEventListener("message", this._handleMessage.bind(this), false); this._features = {}; const params = new URLSearchParams(window.parent.location.search); const deferred: Q.Deferred = Q.defer(); let authType: string = null; // Encrypted token flow if (!!params && params.has("key")) { Main._encryptedToken = encodeURIComponent(params.get("key")); SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken); authType = AuthType.EncryptedToken; } else if (Main._hasCachedEncryptedKey()) { Main._encryptedToken = SessionStorageUtility.getEntryString(StorageKey.EncryptedKeyToken); authType = AuthType.EncryptedToken; } // Aad flow if (AuthHeadersUtil.isUserSignedIn()) { authType = AuthType.AAD; } if (params) { this._features = Main.extractFeatures(params); } (window).authType = authType; if (!authType) { return Q.reject("Sign in needed"); } const explorer: Explorer = this._instantiateExplorer(); if (authType === AuthType.EncryptedToken) { sendMessage({ type: MessageTypes.UpdateAccountSwitch, props: { authType: AuthType.EncryptedToken, displayText: "Loading..." } }); updateUserContext({ accessToken: Main._encryptedToken }); Main._getAccessInputMetadata(Main._encryptedToken).then( () => { const expiryTimestamp: number = Main._accessInputMetadata && parseInt(Main._accessInputMetadata.expiryTimestamp); if (authType === AuthType.EncryptedToken && (isNaN(expiryTimestamp) || expiryTimestamp <= 0)) { return deferred.reject("Token expired"); } Main._initDataExplorerFrameInputs(explorer); deferred.resolve(explorer); }, (error: any) => { console.error(error); deferred.reject(error); } ); } else if (authType === AuthType.AAD) { sendMessage({ type: MessageTypes.GetAccessAadRequest }); if (this._getAadAccessDeferred != null) { // already request aad access, don't duplicate return Q(null); } this._explorer = explorer; this._getAadAccessDeferred = Q.defer(); return this._getAadAccessDeferred.promise.finally(() => { this._getAadAccessDeferred = null; }); } else { Main._initDataExplorerFrameInputs(explorer); deferred.resolve(explorer); } return deferred.promise; } 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 configureTokenValidationDisplayPrompt(explorer: Explorer): void { const authType: AuthType = (window).authType; if ( !explorer || !Main._encryptedToken || !Main._accessInputMetadata || Main._accessInputMetadata.expiryTimestamp == null || authType !== AuthType.EncryptedToken ) { return; } Main._showGuestAccessTokenRenewalPromptInMs(explorer, parseInt(Main._accessInputMetadata.expiryTimestamp)); } public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties { let accountEndpoint: string; let collectionId: string; let databaseId: string; let partitionKey: string; let resourceToken: string; const connectionStringParts = connectionString.split(";"); connectionStringParts.forEach((part: string) => { if (part.startsWith("type=resource")) { resourceToken = part + ";"; } else if (part.startsWith("AccountEndpoint=")) { accountEndpoint = part.substring(16); } else if (part.startsWith("DatabaseId=")) { databaseId = part.substring(11); } else if (part.startsWith("CollectionId=")) { collectionId = part.substring(13); } else if (part.startsWith("PartitionKey=")) { partitionKey = part.substring(13); } else if (part !== "") { resourceToken += part + ";"; } }); return { accountEndpoint, collectionId, databaseId, partitionKey, resourceToken }; } public static renewExplorerAccess = (explorer: Explorer, connectionString: string): Q.Promise => { if (!connectionString) { console.error("Missing or invalid connection string input"); Q.reject("Missing or invalid connection string input"); } if (Main._isResourceToken(connectionString)) { return Main._renewExplorerAccessWithResourceToken(explorer, connectionString); } const deferred: Q.Deferred = Q.defer(); AuthHeadersUtil.generateUnauthenticatedEncryptedTokenForConnectionString(connectionString).then( (encryptedToken: GenerateTokenResponse) => { if (!encryptedToken || !encryptedToken.readWrite) { deferred.reject("Encrypted token is empty or undefined"); } Main._encryptedToken = encryptedToken.readWrite; window.authType = AuthType.EncryptedToken; updateUserContext({ accessToken: Main._encryptedToken }); Main._getAccessInputMetadata(Main._encryptedToken).then( () => { if (explorer.isConnectExplorerVisible()) { explorer.notificationConsoleData([]); explorer.hideConnectExplorerForm(); } if (Main._accessInputMetadata.apiKind != ApiKind.Graph) { // do not save encrypted token for graphs because we cannot extract master key in the client SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken); window.parent && window.parent.history.replaceState( { encryptedToken: encryptedToken }, "", `?key=${Main._encryptedToken}${(window.parent && window.parent.location.hash) || ""}` ); // replace query params if any } else { SessionStorageUtility.removeEntry(StorageKey.EncryptedKeyToken); window.parent && window.parent.history.replaceState( { encryptedToken: encryptedToken }, "", `?${(window.parent && window.parent.location.hash) || ""}` ); // replace query params if any } const masterKey: string = Main._getMasterKeyFromConnectionString(connectionString); Main.configureTokenValidationDisplayPrompt(explorer); Main._setExplorerReady(explorer, masterKey); deferred.resolve(); }, (error: any) => { console.error(error); deferred.reject(error); } ); }, (error: any) => { deferred.reject(`Failed to generate encrypted token: ${getErrorMessage(error)}`); } ); 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 */, account?: DatabaseAccount, authorizationToken?: string /* access key */ ): Q.Promise { const serverId: string = AuthHeadersUtil.serverId; const authType: string = (window).authType; const accountResourceId = authType === AuthType.EncryptedToken ? Main._databaseAccountId : authType === AuthType.AAD && account ? account.id : ""; const subscriptionId: string = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; const resourceGroup: string = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; explorer.isTryCosmosDBSubscription(SubscriptionUtilMappings.FreeTierSubscriptionIds.indexOf(subscriptionId) >= 0); if (authorizationToken && authorizationToken.indexOf("Bearer") !== 0) { // Portal sends the auth token with bearer suffix, so we prepend the same to be consistent authorizationToken = `Bearer ${authorizationToken}`; } if (authType === AuthType.EncryptedToken) { 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, name: Main._accessInputMetadata.accountName, kind: this._getDatabaseAccountKindFromExperience(apiExperience), properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata), tags: { defaultExperience: apiExperience } }, subscriptionId, resourceGroup, masterKey, hasWriteAccess: true, // TODO: we should embed this information in the token ideally authorizationToken: undefined, features: this._features, csmEndpoint: undefined, dnsSuffix: null, serverId: serverId, extensionEndpoint: configContext.BACKEND_ENDPOINT, subscriptionType: CollectionCreation.DefaultSubscriptionType, quotaId: undefined, addCollectionDefaultFlight: explorer.flight(), isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() }); } if (authType === AuthType.AAD) { const inputs: DataExplorerInputsFrame = { databaseAccount: account, subscriptionId, resourceGroup, masterKey, hasWriteAccess: true, //TODO: 425017 - support read access authorizationToken, features: this._features, csmEndpoint: undefined, dnsSuffix: null, serverId: serverId, extensionEndpoint: configContext.BACKEND_ENDPOINT, subscriptionType: CollectionCreation.DefaultSubscriptionType, quotaId: undefined, addCollectionDefaultFlight: explorer.flight(), isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() }; return explorer.initDataExplorerWithFrameInputs(inputs); } if (authType === AuthType.ResourceToken) { const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( Main._accessInputMetadata.apiKind ); return explorer.initDataExplorerWithFrameInputs({ databaseAccount: { id: Main._databaseAccountId, name: Main._accessInputMetadata.accountName, kind: this._getDatabaseAccountKindFromExperience(apiExperience), properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata), tags: { defaultExperience: apiExperience } }, subscriptionId, resourceGroup, masterKey, hasWriteAccess: true, // TODO: we should embed this information in the token ideally authorizationToken: undefined, features: this._features, csmEndpoint: undefined, dnsSuffix: null, serverId: serverId, extensionEndpoint: configContext.BACKEND_ENDPOINT, subscriptionType: CollectionCreation.DefaultSubscriptionType, quotaId: undefined, addCollectionDefaultFlight: explorer.flight(), isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), isAuthWithresourceToken: true }); } return Q.reject(`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 _showGuestAccessTokenRenewalPromptInMs(explorer: Explorer, interval: number): void { if (interval != null && !isNaN(interval)) { setTimeout(() => { explorer.displayGuestAccessTokenRenewalPrompt(); }, interval); } } private static _hasCachedEncryptedKey(): boolean { return SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken); } private static _getDatabaseAccountKindFromExperience(apiExperience: string): string { if (apiExperience === Constants.DefaultAccountExperience.MongoDB) { return Constants.AccountKind.MongoDB; } if (apiExperience === Constants.DefaultAccountExperience.ApiForMongoDB) { return Constants.AccountKind.MongoDB; } 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 _getMasterKeyFromConnectionString(connectionString: string): string { if (!connectionString || Main._accessInputMetadata == null || Main._accessInputMetadata.apiKind !== ApiKind.Graph) { // client only needs master key for Graph API return undefined; } const matchedParts: string[] = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$"); return (matchedParts.length > 1 && matchedParts[1]) || undefined; } private static _isResourceToken(connectionString: string): boolean { return connectionString && connectionString.includes("type=resource"); } private static _renewExplorerAccessWithResourceToken = ( explorer: Explorer, connectionString: string ): Q.Promise => { window.authType = AuthType.ResourceToken; const properties: resourceTokenConnectionStringProperties = Main.parseResourceTokenConnectionString( connectionString ); if ( !properties.accountEndpoint || !properties.resourceToken || !properties.databaseId || !properties.collectionId ) { console.error("Invalid connection string input"); Q.reject("Invalid connection string input"); } updateUserContext({ resourceToken: properties.resourceToken, endpoint: properties.accountEndpoint }); explorer.resourceTokenDatabaseId(properties.databaseId); explorer.resourceTokenCollectionId(properties.collectionId); if (properties.partitionKey) { explorer.resourceTokenPartitionKey(properties.partitionKey); } Main._accessInputMetadata = Main._getAccessInputMetadataFromAccountEndpoint(properties.accountEndpoint); if (explorer.isConnectExplorerVisible()) { explorer.notificationConsoleData([]); explorer.hideConnectExplorerForm(); } Main._setExplorerReady(explorer); return Q.resolve(); }; private static _getAccessInputMetadataFromAccountEndpoint = (accountEndpoint: string): AccessInputMetadata => { const documentEndpoint: string = accountEndpoint; const result: RegExpMatchArray = accountEndpoint.match("https://([^\\.]+)\\..+"); const accountName: string = result && result[1]; const apiEndpoint: string = accountEndpoint.substring(8); const apiKind: number = ApiKind.SQL; return { accountName, apiEndpoint, apiKind, documentEndpoint, expiryTimestamp: "" }; }; private static _setExplorerReady( explorer: Explorer, masterKey?: string, account?: DatabaseAccount, authorizationToken?: string ) { Main._initDataExplorerFrameInputs(explorer, masterKey, account, authorizationToken); 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); } }