mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-21 09:51:11 +00:00
WIP
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { StyleConstants } from "../../../Common/Constants";
|
||||||
import * as React from "react";
|
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 { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||||
import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
||||||
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
||||||
@@ -39,7 +39,8 @@ const buttonStyles: IButtonStyles = {
|
|||||||
|
|
||||||
export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }> = ({ armToken }) => {
|
export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }> = ({ armToken }) => {
|
||||||
const subscriptions = useSubscriptions(armToken);
|
const subscriptions = useSubscriptions(armToken);
|
||||||
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>();
|
const cachedSubscriptionId = localStorage.getItem("cachedSubscriptionId");
|
||||||
|
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>(cachedSubscriptionId);
|
||||||
const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
|
const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
|
||||||
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>();
|
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>();
|
||||||
|
|
||||||
@@ -67,7 +68,9 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
onChange: (event, option) => {
|
onChange: (event, option) => {
|
||||||
setSelectedSubscriptionId(String(option.key));
|
const subscriptionId = String(option.key);
|
||||||
|
setSelectedSubscriptionId(subscriptionId);
|
||||||
|
localStorage.setItem("cachedSubscriptionId", subscriptionId);
|
||||||
},
|
},
|
||||||
defaultSelectedKey: selectedSubscriptionId,
|
defaultSelectedKey: selectedSubscriptionId,
|
||||||
placeholder: "Select subscription from list",
|
placeholder: "Select subscription from list",
|
||||||
@@ -81,7 +84,7 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "switchAccount",
|
key: "switchAccount",
|
||||||
onRender: () => {
|
onRender: (item, dismissMenu) => {
|
||||||
const isLoadingAccounts = false;
|
const isLoadingAccounts = false;
|
||||||
|
|
||||||
const options = accounts.map(account => ({
|
const options = accounts.map(account => ({
|
||||||
@@ -102,6 +105,7 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
|||||||
options,
|
options,
|
||||||
onChange: (event, option) => {
|
onChange: (event, option) => {
|
||||||
setSelectedAccoutName(String(option.key));
|
setSelectedAccoutName(String(option.key));
|
||||||
|
dismissMenu();
|
||||||
},
|
},
|
||||||
defaultSelectedKey: selectedAccountName,
|
defaultSelectedKey: selectedAccountName,
|
||||||
placeholder: placeHolderText,
|
placeholder: placeHolderText,
|
||||||
@@ -116,13 +120,13 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonProps: IButtonProps = {
|
return (
|
||||||
text: selectedAccountName || "Select Database Account",
|
<DefaultButton
|
||||||
menuProps: menuProps,
|
text={selectedAccountName || "Select Database Account"}
|
||||||
styles: buttonStyles,
|
menuProps={menuProps}
|
||||||
className: "accountSwitchButton",
|
styles={buttonStyles}
|
||||||
id: "accountSwitchButton"
|
className="accountSwitchButton"
|
||||||
};
|
id="accountSwitchButton"
|
||||||
|
/>
|
||||||
return <DefaultButton {...buttonProps} />;
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,9 @@ import { AuthType } from "./AuthType";
|
|||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
const msal = new Msal.UserAgentApplication({
|
const msal = new Msal.UserAgentApplication({
|
||||||
|
cache: {
|
||||||
|
cacheLocation: "localStorage"
|
||||||
|
},
|
||||||
auth: {
|
auth: {
|
||||||
authority: "https://login.microsoft.com/common",
|
authority: "https://login.microsoft.com/common",
|
||||||
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
|
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 = () => {
|
const App: React.FunctionComponent = () => {
|
||||||
// Hooks for handling encrypted portal tokens
|
// Hooks for handling encrypted portal tokens
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -51,11 +57,13 @@ const App: React.FunctionComponent = () => {
|
|||||||
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
|
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
|
||||||
|
|
||||||
// Hooks for AAD authentication
|
// Hooks for AAD authentication
|
||||||
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(false);
|
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(
|
||||||
const [account, setAccount] = React.useState<Msal.Account>();
|
Boolean(cachedAccount && cachedTenantId) || false
|
||||||
|
);
|
||||||
|
const [account, setAccount] = React.useState<Msal.Account>(cachedAccount);
|
||||||
|
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
||||||
const [graphToken, setGraphToken] = React.useState<string>();
|
const [graphToken, setGraphToken] = React.useState<string>();
|
||||||
const [armToken, setArmToken] = React.useState<string>();
|
const [armToken, setArmToken] = React.useState<string>();
|
||||||
const [tenantId, setTenantId] = React.useState<string>();
|
|
||||||
const [connectionString, setConnectionString] = React.useState<string>("");
|
const [connectionString, setConnectionString] = React.useState<string>("");
|
||||||
|
|
||||||
const login = React.useCallback(async () => {
|
const login = React.useCallback(async () => {
|
||||||
@@ -63,17 +71,17 @@ const App: React.FunctionComponent = () => {
|
|||||||
setLoggedIn();
|
setLoggedIn();
|
||||||
setAccount(response.account);
|
setAccount(response.account);
|
||||||
setTenantId(response.tenantId);
|
setTenantId(response.tenantId);
|
||||||
|
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logout = React.useCallback(() => {
|
const logout = React.useCallback(() => {
|
||||||
msal.logout();
|
|
||||||
setLoggedOut();
|
setLoggedOut();
|
||||||
|
localStorage.removeItem("cachedTenantId");
|
||||||
|
msal.logout();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (account && tenantId) {
|
if (account && tenantId) {
|
||||||
console.log(msal.authority);
|
|
||||||
console.log("Getting tokens for", tenantId);
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
msal.acquireTokenSilent({
|
msal.acquireTokenSilent({
|
||||||
scopes: ["https://graph.windows.net//.default"]
|
scopes: ["https://graph.windows.net//.default"]
|
||||||
@@ -227,7 +235,6 @@ const App: React.FunctionComponent = () => {
|
|||||||
id="connectWithConnectionString"
|
id="connectWithConnectionString"
|
||||||
onSubmit={async event => {
|
onSubmit={async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// const foo = parseConnectionString(connectionString);
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append(HttpHeaders.connectionString, connectionString);
|
headers.append(HttpHeaders.connectionString, connectionString);
|
||||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
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());
|
const result: GenerateTokenResponse = JSON.parse(await response.json());
|
||||||
console.log(result.readWrite || result.read);
|
console.log(result.readWrite || result.read);
|
||||||
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
|
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
|
||||||
event.preventDefault();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="connectExplorerContent connectStringText">
|
<p className="connectExplorerContent connectStringText">
|
||||||
@@ -315,4 +321,4 @@ const App: React.FunctionComponent = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<App />, document.body);
|
render(<App />, document.getElementById("App"));
|
||||||
|
|||||||
40
src/Main.tsx
40
src/Main.tsx
@@ -55,9 +55,7 @@ import "url-polyfill/url-polyfill.min";
|
|||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
import * as Emulator from "./Platform/Emulator/Main";
|
|
||||||
import Hosted from "./Platform/Hosted/Main";
|
import Hosted from "./Platform/Hosted/Main";
|
||||||
import * as Portal from "./Platform/Portal/Main";
|
|
||||||
import { AuthType } from "./AuthType";
|
import { AuthType } from "./AuthType";
|
||||||
|
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
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 refreshImg from "../images/refresh-cosmos.svg";
|
||||||
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
||||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||||
|
import { AccountKind, DefaultAccountExperience, TagNames } from "./Common/Constants";
|
||||||
|
|
||||||
// TODO: Encapsulate and reuse all global variables as environment variables
|
// TODO: Encapsulate and reuse all global variables as environment variables
|
||||||
window.authType = AuthType.AAD;
|
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 = () => {
|
const App: React.FunctionComponent = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeConfiguration().then(config => {
|
initializeConfiguration().then(config => {
|
||||||
@@ -84,9 +100,25 @@ const App: React.FunctionComponent = () => {
|
|||||||
explorer = Hosted.initializeExplorer();
|
explorer = Hosted.initializeExplorer();
|
||||||
} else if (config.platform === Platform.Emulator) {
|
} else if (config.platform === Platform.Emulator) {
|
||||||
window.authType = AuthType.MasterKey;
|
window.authType = AuthType.MasterKey;
|
||||||
explorer = Emulator.initializeExplorer();
|
const explorer = new Explorer();
|
||||||
|
explorer.databaseAccount(emulatorAccount);
|
||||||
|
explorer.isAccountReady(true);
|
||||||
} else if (config.platform === Platform.Portal) {
|
} 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);
|
applyExplorerBindings(explorer);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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<Array<Tenant>> {
|
|
||||||
let tenants: Array<Tenant> = [];
|
|
||||||
|
|
||||||
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<Array<Subscription>> {
|
|
||||||
let subscriptions: Array<Subscription> = [];
|
|
||||||
|
|
||||||
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<Array<DatabaseAccount>> {
|
|
||||||
if (!subscriptionIds || !subscriptionIds.length) {
|
|
||||||
return Promise.reject("No subscription passed in");
|
|
||||||
}
|
|
||||||
|
|
||||||
let accounts: Array<DatabaseAccount> = [];
|
|
||||||
|
|
||||||
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<DatabaseAccount> {
|
|
||||||
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<AccountKeys> {
|
|
||||||
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<string> {
|
|
||||||
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<Headers> {
|
|
||||||
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[];
|
|
||||||
}
|
|
||||||
@@ -12,16 +12,6 @@ import { userContext } from "../../UserContext";
|
|||||||
export default class AuthHeadersUtil {
|
export default class AuthHeadersUtil {
|
||||||
public static serverId: string = Constants.ServerIds.productionPortal;
|
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<DataModels.AccessInputMetadata> {
|
public static getAccessInputMetadata(accessInput: string): Q.Promise<DataModels.AccessInputMetadata> {
|
||||||
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>();
|
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>();
|
||||||
const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`;
|
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<string> {
|
|
||||||
const AuthorizationType: string = (<any>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<string>(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<Blob> {
|
|
||||||
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<string | undefined> {
|
|
||||||
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 {
|
private static _generateResourceUrl(): string {
|
||||||
const databaseAccount = userContext.databaseAccount;
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const subscriptionId: string = userContext.subscriptionId;
|
const subscriptionId: string = userContext.subscriptionId;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import AuthHeadersUtil from "./Authorization";
|
|||||||
import Q from "q";
|
import Q from "q";
|
||||||
import {
|
import {
|
||||||
AccessInputMetadata,
|
AccessInputMetadata,
|
||||||
AccountKeys,
|
|
||||||
ApiKind,
|
ApiKind,
|
||||||
DatabaseAccount,
|
DatabaseAccount,
|
||||||
GenerateTokenResponse,
|
GenerateTokenResponse,
|
||||||
@@ -11,12 +10,10 @@ import {
|
|||||||
} from "../../Contracts/DataModels";
|
} from "../../Contracts/DataModels";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { CollectionCreation } from "../../Shared/Constants";
|
import { CollectionCreation } from "../../Shared/Constants";
|
||||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
|
||||||
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
||||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||||
import { HostedUtils } from "./HostedUtils";
|
import { HostedUtils } from "./HostedUtils";
|
||||||
import { sendMessage } from "../../Common/MessageHandler";
|
import { sendMessage } from "../../Common/MessageHandler";
|
||||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
|
||||||
import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { SubscriptionUtilMappings } from "../../Shared/Constants";
|
import { SubscriptionUtilMappings } from "../../Shared/Constants";
|
||||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||||
@@ -24,35 +21,17 @@ import Explorer from "../../Explorer/Explorer";
|
|||||||
import { updateUserContext } from "../../UserContext";
|
import { updateUserContext } from "../../UserContext";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import { extractFeatures } from "./extractFeatures";
|
||||||
|
|
||||||
export default class Main {
|
export default class Main {
|
||||||
private static _databaseAccountId: string;
|
private static _databaseAccountId: string;
|
||||||
private static _encryptedToken: string;
|
private static _encryptedToken: string;
|
||||||
private static _accessInputMetadata: AccessInputMetadata;
|
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<Explorer>;
|
|
||||||
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 {
|
public static initializeExplorer(): Explorer {
|
||||||
window.addEventListener("message", this._handleMessage.bind(this), false);
|
|
||||||
this._features = {};
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
let authType: string = params && params.get("authType");
|
let authType: string = params && params.get("authType");
|
||||||
|
|
||||||
if (params) {
|
|
||||||
this._features = Main.extractFeatures(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypted token flow
|
// Encrypted token flow
|
||||||
if (params && params.has("key")) {
|
if (params && params.has("key")) {
|
||||||
Main._encryptedToken = encodeURIComponent(params.get("key"));
|
Main._encryptedToken = encodeURIComponent(params.get("key"));
|
||||||
@@ -60,39 +39,22 @@ export default class Main {
|
|||||||
authType = AuthType.EncryptedToken;
|
authType = AuthType.EncryptedToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
(<any>window).authType = authType;
|
const explorer = new Explorer();
|
||||||
if (!authType) {
|
// workaround to resolve cyclic refs with view // TODO. Is this even needed anymore?
|
||||||
throw new Error("Sign in needed");
|
explorer.renewExplorerShareAccess = Main.renewExplorerAccess;
|
||||||
}
|
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||||
|
|
||||||
const explorer: Explorer = this._instantiateExplorer();
|
|
||||||
if (authType === AuthType.EncryptedToken) {
|
if (authType === AuthType.EncryptedToken) {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
accessToken: Main._encryptedToken
|
accessToken: Main._encryptedToken
|
||||||
});
|
});
|
||||||
Main._initDataExplorerFrameInputs(explorer);
|
Main._initDataExplorerFrameInputs(explorer);
|
||||||
} else if (authType === AuthType.AAD) {
|
} else if (authType === AuthType.AAD) {
|
||||||
this._explorer = explorer;
|
|
||||||
} else {
|
} else {
|
||||||
Main._initDataExplorerFrameInputs(explorer);
|
Main._initDataExplorerFrameInputs(explorer);
|
||||||
}
|
}
|
||||||
return 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 {
|
public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties {
|
||||||
let accountEndpoint: string;
|
let accountEndpoint: string;
|
||||||
let collectionId: string;
|
let collectionId: string;
|
||||||
@@ -193,16 +155,6 @@ export default class Main {
|
|||||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
public static getUninitializedExplorerForGuestAccess(): Explorer {
|
|
||||||
const explorer = Main._instantiateExplorer();
|
|
||||||
if (window.authType === AuthType.AAD) {
|
|
||||||
this._explorer = explorer;
|
|
||||||
}
|
|
||||||
(<any>window).dataExplorer = explorer;
|
|
||||||
|
|
||||||
return explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static _initDataExplorerFrameInputs(
|
private static _initDataExplorerFrameInputs(
|
||||||
explorer: Explorer,
|
explorer: Explorer,
|
||||||
masterKey?: string /* master key extracted from connection string if available */,
|
masterKey?: string /* master key extracted from connection string if available */,
|
||||||
@@ -230,13 +182,6 @@ export default class Main {
|
|||||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||||
Main._accessInputMetadata.apiKind
|
Main._accessInputMetadata.apiKind
|
||||||
);
|
);
|
||||||
sendMessage({
|
|
||||||
type: MessageTypes.UpdateAccountSwitch,
|
|
||||||
props: {
|
|
||||||
authType: AuthType.EncryptedToken,
|
|
||||||
selectedAccountName: Main._accessInputMetadata.accountName
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return explorer.initDataExplorerWithFrameInputs({
|
return explorer.initDataExplorerWithFrameInputs({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
id: Main._databaseAccountId,
|
id: Main._databaseAccountId,
|
||||||
@@ -250,7 +195,7 @@ export default class Main {
|
|||||||
masterKey,
|
masterKey,
|
||||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||||
authorizationToken: undefined,
|
authorizationToken: undefined,
|
||||||
features: this._features,
|
features: extractFeatures(),
|
||||||
csmEndpoint: undefined,
|
csmEndpoint: undefined,
|
||||||
dnsSuffix: null,
|
dnsSuffix: null,
|
||||||
serverId: serverId,
|
serverId: serverId,
|
||||||
@@ -270,7 +215,7 @@ export default class Main {
|
|||||||
masterKey,
|
masterKey,
|
||||||
hasWriteAccess: true, //TODO: 425017 - support read access
|
hasWriteAccess: true, //TODO: 425017 - support read access
|
||||||
authorizationToken,
|
authorizationToken,
|
||||||
features: this._features,
|
features: extractFeatures(),
|
||||||
csmEndpoint: undefined,
|
csmEndpoint: undefined,
|
||||||
dnsSuffix: null,
|
dnsSuffix: null,
|
||||||
serverId: serverId,
|
serverId: serverId,
|
||||||
@@ -300,7 +245,7 @@ export default class Main {
|
|||||||
masterKey,
|
masterKey,
|
||||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||||
authorizationToken: undefined,
|
authorizationToken: undefined,
|
||||||
features: this._features,
|
features: extractFeatures(),
|
||||||
csmEndpoint: undefined,
|
csmEndpoint: undefined,
|
||||||
dnsSuffix: null,
|
dnsSuffix: null,
|
||||||
serverId: serverId,
|
serverId: serverId,
|
||||||
@@ -316,32 +261,6 @@ export default class Main {
|
|||||||
throw new Error(`Unsupported AuthType ${authType}`);
|
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 {
|
private static _getDatabaseAccountKindFromExperience(apiExperience: string): string {
|
||||||
if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
|
if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||||
return Constants.AccountKind.MongoDB;
|
return Constants.AccountKind.MongoDB;
|
||||||
@@ -354,19 +273,9 @@ export default class Main {
|
|||||||
return Constants.AccountKind.GlobalDocumentDB;
|
return Constants.AccountKind.GlobalDocumentDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _getAccessInputMetadata(accessInput: string): Q.Promise<void> {
|
private static async _getAccessInputMetadata(accessInput: string): Promise<void> {
|
||||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
const metadata = await AuthHeadersUtil.getAccessInputMetadata(accessInput);
|
||||||
AuthHeadersUtil.getAccessInputMetadata(accessInput).then(
|
|
||||||
(metadata: any) => {
|
|
||||||
Main._accessInputMetadata = metadata;
|
Main._accessInputMetadata = metadata;
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _getMasterKeyFromConnectionString(connectionString: string): string {
|
private static _getMasterKeyFromConnectionString(connectionString: string): string {
|
||||||
@@ -447,84 +356,4 @@ export default class Main {
|
|||||||
explorer.isAccountReady.valueHasMutated();
|
explorer.isAccountReady.valueHasMutated();
|
||||||
sendMessage("ready");
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,6 @@
|
|||||||
import Main from "./Main";
|
import Main from "./Main";
|
||||||
|
|
||||||
describe("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", () => {
|
it("correctly parses resource token connection string", () => {
|
||||||
const connectionString =
|
const connectionString =
|
||||||
"AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;";
|
"AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;";
|
||||||
|
|||||||
17
src/Platform/Hosted/extractFeatures.test.ts
Normal file
17
src/Platform/Hosted/extractFeatures.test.ts
Normal file
@@ -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"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
16
src/Platform/Hosted/extractFeatures.ts
Normal file
16
src/Platform/Hosted/extractFeatures.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 { AuthType } from "../AuthType";
|
||||||
|
import * as Constants from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
import { configContext, Platform } from "../ConfigContext";
|
||||||
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
|
||||||
|
|
||||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||||
if (window.authType === AuthType.EncryptedToken) {
|
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<string> {
|
|
||||||
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) {
|
export function decryptJWTToken(token: string) {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");
|
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div id="App"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user