diff --git a/src/AuthType.ts b/src/AuthType.ts index 4d5428ede..9cc33f789 100644 --- a/src/AuthType.ts +++ b/src/AuthType.ts @@ -2,5 +2,6 @@ export enum AuthType { AAD = "aad", EncryptedToken = "encryptedtoken", MasterKey = "masterkey", - ResourceToken = "resourcetoken" + ResourceToken = "resourcetoken", + ConnectionString = "connectionstring" } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 505090c18..6e68bf9ee 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -587,11 +587,3 @@ export interface MemoryUsageInfo { freeKB: number; totalKB: number; } - -export interface resourceTokenConnectionStringProperties { - accountEndpoint: string; - collectionId: string; - databaseId: string; - partitionKey?: string; - resourceToken: string; -} diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 6bd92fa97..09db31f57 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -17,15 +17,10 @@ import "./Shared/appInsights"; import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; import { useAADAuth } from "./hooks/useAADAuth"; import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; +import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; initializeIcons(); -interface HostedExplorerChildFrame extends Window { - authType: AuthType; - databaseAccount: DatabaseAccount; - authorizationToken: string; -} - const App: React.FunctionComponent = () => { // For handling encrypted portal tokens sent via query paramter const params = new URLSearchParams(window.location.search); @@ -37,6 +32,8 @@ const App: React.FunctionComponent = () => { const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login } = useAADAuth(); const [databaseAccount, setDatabaseAccount] = React.useState(); + const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); + const [connectionString, setConnectionString] = React.useState(); const ref = React.useRef(); @@ -46,14 +43,31 @@ const App: React.FunctionComponent = () => { // In hosted mode, we can set global properties directly on the child iframe. // This is not possible in the portal where the iframes have different origins const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame; - frameWindow.authType = AuthType.AAD; - frameWindow.databaseAccount = databaseAccount; - frameWindow.authorizationToken = armToken; - // const frameWindow = ref.current.contentWindow; - // frameWindow.authType = AuthType.EncryptedToken; - // frameWindow.encryptedToken = encryptedToken; - // frameWindow.encryptedTokenMetadata = encryptedTokenMetadata; - // frameWindow.parsedConnectionString = "foo"; + // AAD authenticated uses ALWAYS using AAD authType + if (isLoggedIn) { + frameWindow.hostedConfig = { + authType: AuthType.AAD, + databaseAccount, + authorizationToken: armToken + }; + } else if (authType === AuthType.EncryptedToken) { + frameWindow.hostedConfig = { + authType: AuthType.EncryptedToken, + encryptedToken, + encryptedTokenMetadata + }; + } else if (authType === AuthType.ConnectionString) { + frameWindow.hostedConfig = { + authType: AuthType.ConnectionString, + encryptedToken, + encryptedTokenMetadata + }; + } else if (authType === AuthType.ResourceToken) { + frameWindow.hostedConfig = { + authType: AuthType.ResourceToken, + resourceToken: connectionString + }; + } } }, [ref, encryptedToken, encryptedTokenMetadata, isLoggedIn, databaseAccount]); @@ -95,23 +109,26 @@ const App: React.FunctionComponent = () => { - {databaseAccount && ( - // Ideally we would import and render data explorer like any other React component, however - // because it still has a significant amount of Knockout code, this would lead to memory leaks. - // Knockout does not have a way to tear down all of its binding and listeners with a single method. - // It's possible this can be changed once all knockout code has been removed. - + {(isLoggedIn && databaseAccount) || + (encryptedTokenMetadata && encryptedTokenMetadata && ( + // Ideally we would import and render data explorer like any other React component, however + // because it still has a significant amount of Knockout code, this would lead to memory leaks. + // Knockout does not have a way to tear down all of its binding and listeners with a single method. + // It's possible this can be changed once all knockout code has been removed. + + ))} + {!isLoggedIn && !encryptedTokenMetadata && ( + )} - {!isLoggedIn && !encryptedTokenMetadata && } {isLoggedIn && } ); diff --git a/src/HostedExplorerChildFrame.ts b/src/HostedExplorerChildFrame.ts new file mode 100644 index 000000000..70497fb2c --- /dev/null +++ b/src/HostedExplorerChildFrame.ts @@ -0,0 +1,32 @@ +import { AuthType } from "./AuthType"; +import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels"; + +export interface HostedExplorerChildFrame extends Window { + hostedConfig: AAD | ConnectionString | EncryptedToken | ResourceToken; +} + +interface AAD { + authType: AuthType.AAD; + databaseAccount: DatabaseAccount; + authorizationToken: string; +} + +interface ConnectionString { + authType: AuthType.ConnectionString; + // Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy + encryptedToken: string; + encryptedTokenMetadata: AccessInputMetadata; + // Master key is currently only used by Graph API. All other APIs use encrypted tokens and proxy with connection string + masterKey?: string; +} + +interface EncryptedToken { + authType: AuthType.EncryptedToken; + encryptedToken: string; + encryptedTokenMetadata: AccessInputMetadata; +} + +interface ResourceToken { + authType: AuthType.ResourceToken; + resourceToken: string; +} diff --git a/src/Main.tsx b/src/Main.tsx index 16dc8bb4f..00866c611 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -75,22 +75,73 @@ import AuthHeadersUtil from "./Platform/Hosted/Authorization"; import { CollectionCreation } from "./Shared/Constants"; import { extractFeatures } from "./Platform/Hosted/extractFeatures"; import { emulatorAccount } from "./Platform/Emulator/emulatorAccount"; +import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; +import { + getDatabaseAccountKindFromExperience, + getDatabaseAccountPropertiesFromMetadata +} from "./Platform/Hosted/HostedUtils"; // TODO: Encapsulate and reuse all global variables as environment variables window.authType = AuthType.AAD; +// 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]; + const App: React.FunctionComponent = () => { useEffect(() => { initializeConfiguration().then(config => { let explorer: Explorer; if (config.platform === Platform.Hosted) { - const authType: AuthType = window.authType; + const win = (window as unknown) as HostedExplorerChildFrame; explorer = new Explorer(); - if (window.authType === AuthType.EncryptedToken) { + if (win.hostedConfig.authType === AuthType.EncryptedToken) { + // TODO: Remove window.authType + window.authType = AuthType.EncryptedToken; + // Impossible to tell if this is a try cosmos sub using an encrypted token + explorer.isTryCosmosDBSubscription(false); updateUserContext({ - accessToken: window.encryptedToken + accessToken: encodeURIComponent(win.hostedConfig.encryptedToken) }); - Hosted.initDataExplorerFrameInputs(explorer); + + // const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( + // Main._accessInputMetadata.apiKind + // ); + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: { + id: "", + // id: Main._databaseAccountId, + name: win.hostedConfig.encryptedTokenMetadata.accountName, + kind: "", + kind: getDatabaseAccountKindFromExperience(win.hostedConfig.encryptedTokenMetadata.apiKind), + properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), + tags: [] + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: undefined, + hasWriteAccess: true, // TODO: we should embed this information in the token ideally + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: AuthHeadersUtil.serverId, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() + }); + explorer.isAccountReady(true); + } else if (window.authType === AuthType.ResourceToken) { + } else if (window.authType === AuthType.ConnectionString) { } else if (window.authType === AuthType.AAD) { const account = window.databaseAccount; const serverId = AuthHeadersUtil.serverId; @@ -118,7 +169,7 @@ const App: React.FunctionComponent = () => { } } else if (config.platform === Platform.Emulator) { window.authType = AuthType.MasterKey; - const explorer = new Explorer(); + explorer = new Explorer(); explorer.databaseAccount(emulatorAccount); explorer.isAccountReady(true); } else if (config.platform === Platform.Portal) { diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index bf6cb26af..dd774b420 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -3,15 +3,25 @@ import { useBoolean } from "@uifabric/react-hooks"; import { HttpHeaders } from "../../../Common/Constants"; import { GenerateTokenResponse } from "../../../Contracts/DataModels"; import { configContext } from "../../../ConfigContext"; +import { AuthType } from "../../../AuthType"; +import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; interface Props { + connectionString: string; login: () => void; setEncryptedToken: (token: string) => void; + setConnectionString: (connectionString: string) => void; + setAuthType: (authType: AuthType) => void; } -export const ConnectExplorer: React.FunctionComponent = ({ setEncryptedToken, login }: Props) => { - const [connectionString, setConnectionString] = React.useState(""); - const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false); +export const ConnectExplorer: React.FunctionComponent = ({ + setEncryptedToken, + login, + setAuthType, + connectionString, + setConnectionString +}: Props) => { + const [isFormVisible, { setTrue: showForm }] = useBoolean(false); return (
@@ -21,11 +31,17 @@ export const ConnectExplorer: React.FunctionComponent = ({ setEncryptedTo Azure Cosmos DB

Welcome to Azure Cosmos DB

- {isConnectionStringVisible ? ( + {isFormVisible ? (
{ event.preventDefault(); + + if (isResourceTokenConnectionString(connectionString)) { + setAuthType(AuthType.ResourceToken); + return; + } + const headers = new Headers(); headers.append(HttpHeaders.connectionString, connectionString); const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken"; @@ -37,6 +53,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ setEncryptedTo const result: GenerateTokenResponse = JSON.parse(await response.json()); console.log(result.readWrite || result.read); setEncryptedToken(decodeURIComponent(result.readWrite || result.read)); + setAuthType(AuthType.ConnectionString); }} >

Connect to your account with connection string

@@ -66,7 +83,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ setEncryptedTo ) : (
-

+

Connect to your account with connection string

diff --git a/src/Platform/Hosted/Maint.test.ts b/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts similarity index 55% rename from src/Platform/Hosted/Maint.test.ts rename to src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts index 28ca1d7df..12cc90706 100644 --- a/src/Platform/Hosted/Maint.test.ts +++ b/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts @@ -1,10 +1,10 @@ -import Main from "./Main"; +import { isResourceTokenConnectionString, parseResourceTokenConnectionString } from "./ResourceTokenUtils"; -describe("Main", () => { +describe("parseResourceTokenConnectionString", () => { it("correctly parses resource token connection string", () => { const connectionString = "AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"; - const properties = Main.parseResourceTokenConnectionString(connectionString); + const properties = parseResourceTokenConnectionString(connectionString); expect(properties).toEqual({ accountEndpoint: "fakeEndpoint", @@ -18,7 +18,7 @@ describe("Main", () => { it("correctly parses resource token connection string with partition key", () => { const connectionString = "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;PartitionKey=fakePartitionKey;"; - const properties = Main.parseResourceTokenConnectionString(connectionString); + const properties = parseResourceTokenConnectionString(connectionString); expect(properties).toEqual({ accountEndpoint: "fakeEndpoint", @@ -29,3 +29,16 @@ describe("Main", () => { }); }); }); + +describe("isResourceToken", () => { + it("valid resource connection string", () => { + const connectionString = + "AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"; + expect(isResourceTokenConnectionString(connectionString)).toBe(true); + }); + + it("non-resource connection string", () => { + const connectionString = "AccountEndpoint=https://stfaul-sql.documents.azure.com:443/;AccountKey=foo;"; + expect(isResourceTokenConnectionString(connectionString)).toBe(false); + }); +}); diff --git a/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts new file mode 100644 index 000000000..8fb6f51c7 --- /dev/null +++ b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts @@ -0,0 +1,43 @@ +export interface ParsedResourceTokenConnectionString { + accountEndpoint: string; + collectionId: string; + databaseId: string; + partitionKey?: string; + resourceToken: string; +} + +export function parseResourceTokenConnectionString(connectionString: string): ParsedResourceTokenConnectionString { + 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 + }; +} + +export function isResourceTokenConnectionString(connectionString: string): boolean { + return connectionString && connectionString.includes("type=resource"); +} diff --git a/src/Platform/Hosted/HostedUtils.ts b/src/Platform/Hosted/HostedUtils.ts index 828ce7bf1..ed0410e3b 100644 --- a/src/Platform/Hosted/HostedUtils.ts +++ b/src/Platform/Hosted/HostedUtils.ts @@ -31,3 +31,15 @@ export function getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMe } return properties; } + +export function 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; +} diff --git a/src/Platform/Hosted/Main.ts b/src/Platform/Hosted/Main.ts index 5d722c650..ca66480ea 100644 --- a/src/Platform/Hosted/Main.ts +++ b/src/Platform/Hosted/Main.ts @@ -1,7 +1,7 @@ import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; import { configContext } from "../../ConfigContext"; -import { ApiKind, DatabaseAccount, resourceTokenConnectionStringProperties } from "../../Contracts/DataModels"; +import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels"; import { DataExplorerInputsFrame } from "../../Contracts/ViewModels"; import Explorer from "../../Explorer/Explorer"; import "../../Explorer/Tables/DataTable/DataTableBindingManager"; @@ -12,38 +12,6 @@ import { extractFeatures } from "./extractFeatures"; import { getDatabaseAccountPropertiesFromMetadata } from "./HostedUtils"; export default class Main { - 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 initDataExplorerFrameInputs( explorer: Explorer, masterKey?: string /* master key extracted from connection string if available */, @@ -68,6 +36,8 @@ export default class Main { } if (authType === AuthType.EncryptedToken) { + // TODO: Remove window.authType + window.authType = AuthType.EncryptedToken; const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( Main._accessInputMetadata.apiKind ); @@ -150,29 +120,8 @@ export default class Main { throw new Error(`Unsupported AuthType ${authType}`); } - 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 _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"); - } }