WIP, checkpoint

This commit is contained in:
Steve Faulkner
2021-01-03 23:07:26 -06:00
parent 2089a8881d
commit 33f7ae1e6d
10 changed files with 234 additions and 107 deletions

View File

@@ -2,5 +2,6 @@ export enum AuthType {
AAD = "aad",
EncryptedToken = "encryptedtoken",
MasterKey = "masterkey",
ResourceToken = "resourcetoken"
ResourceToken = "resourcetoken",
ConnectionString = "connectionstring"
}

View File

@@ -587,11 +587,3 @@ export interface MemoryUsageInfo {
freeKB: number;
totalKB: number;
}
export interface resourceTokenConnectionStringProperties {
accountEndpoint: string;
collectionId: string;
databaseId: string;
partitionKey?: string;
resourceToken: string;
}

View File

@@ -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<DatabaseAccount>();
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
const [connectionString, setConnectionString] = React.useState<string>();
const ref = React.useRef<HTMLIFrameElement>();
@@ -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 = () => {
</div>
</div>
</header>
{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.
<iframe
// Setting key is needed so React will re-render this element on any account change
key={databaseAccount.id}
ref={ref}
id="explorerMenu"
name="explorer"
className="iframe"
title="explorer"
src="explorer.html?v=1.0.1&platform=Hosted"
></iframe>
{(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.
<iframe
// Setting key is needed so React will re-render this element on any account change
key={databaseAccount?.id || encryptedTokenMetadata?.accountName}
ref={ref}
id="explorerMenu"
name="explorer"
className="iframe"
title="explorer"
src="explorer.html?v=1.0.1&platform=Hosted"
></iframe>
))}
{!isLoggedIn && !encryptedTokenMetadata && (
<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />
)}
{!isLoggedIn && !encryptedTokenMetadata && <ConnectExplorer {...{ login, setEncryptedToken }} />}
{isLoggedIn && <DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId }} />}
</>
);

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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<Props> = ({ setEncryptedToken, login }: Props) => {
const [connectionString, setConnectionString] = React.useState<string>("");
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
export const ConnectExplorer: React.FunctionComponent<Props> = ({
setEncryptedToken,
login,
setAuthType,
connectionString,
setConnectionString
}: Props) => {
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
return (
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
@@ -21,11 +31,17 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({ setEncryptedTo
<img src="images/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" />
</p>
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
{isConnectionStringVisible ? (
{isFormVisible ? (
<form
id="connectWithConnectionString"
onSubmit={async event => {
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<Props> = ({ setEncryptedTo
const result: GenerateTokenResponse = JSON.parse(await response.json());
console.log(result.readWrite || result.read);
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
setAuthType(AuthType.ConnectionString);
}}
>
<p className="connectExplorerContent connectStringText">Connect to your account with connection string</p>
@@ -66,7 +83,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({ setEncryptedTo
) : (
<div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
<p className="switchConnectTypeText" onClick={showConnectionString}>
<p className="switchConnectTypeText" onClick={showForm}>
Connect to your account with connection string
</p>
</div>

View File

@@ -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);
});
});

View File

@@ -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");
}

View File

@@ -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;
}

View File

@@ -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");
}
}