mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-21 01:41:31 +00:00
WIP
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as React from "react";
|
||||
import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
|
||||
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
|
||||
import { useSubscriptions } from "../../../hooks/useSubscriptions";
|
||||
@@ -39,7 +39,8 @@ const buttonStyles: IButtonStyles = {
|
||||
|
||||
export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string }> = ({ armToken }) => {
|
||||
const subscriptions = useSubscriptions(armToken);
|
||||
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>();
|
||||
const cachedSubscriptionId = localStorage.getItem("cachedSubscriptionId");
|
||||
const [selectedSubscriptionId, setSelectedSubscriptionId] = React.useState<string>(cachedSubscriptionId);
|
||||
const accounts = useDatabaseAccounts(selectedSubscriptionId, armToken);
|
||||
const [selectedAccountName, setSelectedAccoutName] = React.useState<string>();
|
||||
|
||||
@@ -67,7 +68,9 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
||||
};
|
||||
}),
|
||||
onChange: (event, option) => {
|
||||
setSelectedSubscriptionId(String(option.key));
|
||||
const subscriptionId = String(option.key);
|
||||
setSelectedSubscriptionId(subscriptionId);
|
||||
localStorage.setItem("cachedSubscriptionId", subscriptionId);
|
||||
},
|
||||
defaultSelectedKey: selectedSubscriptionId,
|
||||
placeholder: "Select subscription from list",
|
||||
@@ -81,7 +84,7 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
||||
},
|
||||
{
|
||||
key: "switchAccount",
|
||||
onRender: () => {
|
||||
onRender: (item, dismissMenu) => {
|
||||
const isLoadingAccounts = false;
|
||||
|
||||
const options = accounts.map(account => ({
|
||||
@@ -102,6 +105,7 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
||||
options,
|
||||
onChange: (event, option) => {
|
||||
setSelectedAccoutName(String(option.key));
|
||||
dismissMenu();
|
||||
},
|
||||
defaultSelectedKey: selectedAccountName,
|
||||
placeholder: placeHolderText,
|
||||
@@ -116,13 +120,13 @@ export const AccountSwitchComponent: React.FunctionComponent<{ armToken: string
|
||||
]
|
||||
};
|
||||
|
||||
const buttonProps: IButtonProps = {
|
||||
text: selectedAccountName || "Select Database Account",
|
||||
menuProps: menuProps,
|
||||
styles: buttonStyles,
|
||||
className: "accountSwitchButton",
|
||||
id: "accountSwitchButton"
|
||||
};
|
||||
|
||||
return <DefaultButton {...buttonProps} />;
|
||||
return (
|
||||
<DefaultButton
|
||||
text={selectedAccountName || "Select Database Account"}
|
||||
menuProps={menuProps}
|
||||
styles={buttonStyles}
|
||||
className="accountSwitchButton"
|
||||
id="accountSwitchButton"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,9 @@ import { AuthType } from "./AuthType";
|
||||
initializeIcons();
|
||||
|
||||
const msal = new Msal.UserAgentApplication({
|
||||
cache: {
|
||||
cacheLocation: "localStorage"
|
||||
},
|
||||
auth: {
|
||||
authority: "https://login.microsoft.com/common",
|
||||
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
|
||||
@@ -40,6 +43,9 @@ const msal = new Msal.UserAgentApplication({
|
||||
}
|
||||
});
|
||||
|
||||
const cachedAccount = msal.getAllAccounts()?.[0];
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
// Hooks for handling encrypted portal tokens
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -51,11 +57,13 @@ const App: React.FunctionComponent = () => {
|
||||
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
|
||||
|
||||
// Hooks for AAD authentication
|
||||
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(false);
|
||||
const [account, setAccount] = React.useState<Msal.Account>();
|
||||
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(
|
||||
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 [armToken, setArmToken] = React.useState<string>();
|
||||
const [tenantId, setTenantId] = React.useState<string>();
|
||||
const [connectionString, setConnectionString] = React.useState<string>("");
|
||||
|
||||
const login = React.useCallback(async () => {
|
||||
@@ -63,17 +71,17 @@ const App: React.FunctionComponent = () => {
|
||||
setLoggedIn();
|
||||
setAccount(response.account);
|
||||
setTenantId(response.tenantId);
|
||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||
}, []);
|
||||
|
||||
const logout = React.useCallback(() => {
|
||||
msal.logout();
|
||||
setLoggedOut();
|
||||
localStorage.removeItem("cachedTenantId");
|
||||
msal.logout();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (account && tenantId) {
|
||||
console.log(msal.authority);
|
||||
console.log("Getting tokens for", tenantId);
|
||||
Promise.all([
|
||||
msal.acquireTokenSilent({
|
||||
scopes: ["https://graph.windows.net//.default"]
|
||||
@@ -227,7 +235,6 @@ const App: React.FunctionComponent = () => {
|
||||
id="connectWithConnectionString"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault();
|
||||
// const foo = parseConnectionString(connectionString);
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
@@ -239,7 +246,6 @@ const App: React.FunctionComponent = () => {
|
||||
const result: GenerateTokenResponse = JSON.parse(await response.json());
|
||||
console.log(result.readWrite || result.read);
|
||||
setEncryptedToken(decodeURIComponent(result.readWrite || result.read));
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<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();
|
||||
|
||||
import * as Emulator from "./Platform/Emulator/Main";
|
||||
import Hosted from "./Platform/Hosted/Main";
|
||||
import * as Portal from "./Platform/Portal/Main";
|
||||
import { AuthType } from "./AuthType";
|
||||
|
||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||
@@ -72,10 +70,28 @@ import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import refreshImg from "../images/refresh-cosmos.svg";
|
||||
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||
import { AccountKind, DefaultAccountExperience, TagNames } from "./Common/Constants";
|
||||
|
||||
// TODO: Encapsulate and reuse all global variables as environment variables
|
||||
window.authType = AuthType.AAD;
|
||||
|
||||
const emulatorAccount = {
|
||||
name: "",
|
||||
id: "",
|
||||
location: "",
|
||||
type: "",
|
||||
kind: AccountKind.DocumentDB,
|
||||
tags: {
|
||||
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
|
||||
},
|
||||
properties: {
|
||||
documentEndpoint: "",
|
||||
tableEndpoint: "",
|
||||
gremlinEndpoint: "",
|
||||
cassandraEndpoint: ""
|
||||
}
|
||||
};
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
useEffect(() => {
|
||||
initializeConfiguration().then(config => {
|
||||
@@ -84,9 +100,25 @@ const App: React.FunctionComponent = () => {
|
||||
explorer = Hosted.initializeExplorer();
|
||||
} else if (config.platform === Platform.Emulator) {
|
||||
window.authType = AuthType.MasterKey;
|
||||
explorer = Emulator.initializeExplorer();
|
||||
const explorer = new Explorer();
|
||||
explorer.databaseAccount(emulatorAccount);
|
||||
explorer.isAccountReady(true);
|
||||
} else if (config.platform === Platform.Portal) {
|
||||
explorer = Portal.initializeExplorer();
|
||||
explorer = new Explorer();
|
||||
|
||||
// In development mode, try to load the iframe message from session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
|
||||
if (initMessage) {
|
||||
const message = JSON.parse(initMessage);
|
||||
console.warn("Loaded cached portal iframe message from session storage");
|
||||
console.dir(message);
|
||||
explorer.initDataExplorerWithFrameInputs(message);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||
}
|
||||
applyExplorerBindings(explorer);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
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> {
|
||||
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.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 {
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const subscriptionId: string = userContext.subscriptionId;
|
||||
|
||||
@@ -3,7 +3,6 @@ import AuthHeadersUtil from "./Authorization";
|
||||
import Q from "q";
|
||||
import {
|
||||
AccessInputMetadata,
|
||||
AccountKeys,
|
||||
ApiKind,
|
||||
DatabaseAccount,
|
||||
GenerateTokenResponse,
|
||||
@@ -11,12 +10,10 @@ import {
|
||||
} from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { CollectionCreation } from "../../Shared/Constants";
|
||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { HostedUtils } from "./HostedUtils";
|
||||
import { sendMessage } from "../../Common/MessageHandler";
|
||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||
import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { SubscriptionUtilMappings } from "../../Shared/Constants";
|
||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||
@@ -24,35 +21,17 @@ import Explorer from "../../Explorer/Explorer";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import { extractFeatures } from "./extractFeatures";
|
||||
|
||||
export default class Main {
|
||||
private static _databaseAccountId: string;
|
||||
private static _encryptedToken: string;
|
||||
private static _accessInputMetadata: AccessInputMetadata;
|
||||
private static _features: { [key: string]: string };
|
||||
// For AAD, Need to post message to hosted frame to do the auth
|
||||
// Use local deferred variable as work around until we find better solution
|
||||
private static _getAadAccessDeferred: Q.Deferred<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 {
|
||||
window.addEventListener("message", this._handleMessage.bind(this), false);
|
||||
this._features = {};
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let authType: string = params && params.get("authType");
|
||||
|
||||
if (params) {
|
||||
this._features = Main.extractFeatures(params);
|
||||
}
|
||||
|
||||
// Encrypted token flow
|
||||
if (params && params.has("key")) {
|
||||
Main._encryptedToken = encodeURIComponent(params.get("key"));
|
||||
@@ -60,39 +39,22 @@ export default class Main {
|
||||
authType = AuthType.EncryptedToken;
|
||||
}
|
||||
|
||||
(<any>window).authType = authType;
|
||||
if (!authType) {
|
||||
throw new Error("Sign in needed");
|
||||
}
|
||||
|
||||
const explorer: Explorer = this._instantiateExplorer();
|
||||
const explorer = new Explorer();
|
||||
// workaround to resolve cyclic refs with view // TODO. Is this even needed anymore?
|
||||
explorer.renewExplorerShareAccess = Main.renewExplorerAccess;
|
||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||
if (authType === AuthType.EncryptedToken) {
|
||||
updateUserContext({
|
||||
accessToken: Main._encryptedToken
|
||||
});
|
||||
Main._initDataExplorerFrameInputs(explorer);
|
||||
} else if (authType === AuthType.AAD) {
|
||||
this._explorer = explorer;
|
||||
} else {
|
||||
Main._initDataExplorerFrameInputs(explorer);
|
||||
}
|
||||
return explorer;
|
||||
}
|
||||
|
||||
public static extractFeatures(params: URLSearchParams): { [key: string]: string } {
|
||||
const featureParamRegex = /feature.(.*)/i;
|
||||
const features: { [key: string]: string } = {};
|
||||
params.forEach((value: string, param: string) => {
|
||||
if (featureParamRegex.test(param)) {
|
||||
const matches: string[] = param.match(featureParamRegex);
|
||||
if (matches.length > 0) {
|
||||
features[matches[1].toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return features;
|
||||
}
|
||||
|
||||
public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties {
|
||||
let accountEndpoint: string;
|
||||
let collectionId: string;
|
||||
@@ -193,16 +155,6 @@ export default class Main {
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
};
|
||||
|
||||
public static getUninitializedExplorerForGuestAccess(): Explorer {
|
||||
const explorer = Main._instantiateExplorer();
|
||||
if (window.authType === AuthType.AAD) {
|
||||
this._explorer = explorer;
|
||||
}
|
||||
(<any>window).dataExplorer = explorer;
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
private static _initDataExplorerFrameInputs(
|
||||
explorer: Explorer,
|
||||
masterKey?: string /* master key extracted from connection string if available */,
|
||||
@@ -230,13 +182,6 @@ export default class Main {
|
||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||
Main._accessInputMetadata.apiKind
|
||||
);
|
||||
sendMessage({
|
||||
type: MessageTypes.UpdateAccountSwitch,
|
||||
props: {
|
||||
authType: AuthType.EncryptedToken,
|
||||
selectedAccountName: Main._accessInputMetadata.accountName
|
||||
}
|
||||
});
|
||||
return explorer.initDataExplorerWithFrameInputs({
|
||||
databaseAccount: {
|
||||
id: Main._databaseAccountId,
|
||||
@@ -250,7 +195,7 @@ export default class Main {
|
||||
masterKey,
|
||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||
authorizationToken: undefined,
|
||||
features: this._features,
|
||||
features: extractFeatures(),
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: null,
|
||||
serverId: serverId,
|
||||
@@ -270,7 +215,7 @@ export default class Main {
|
||||
masterKey,
|
||||
hasWriteAccess: true, //TODO: 425017 - support read access
|
||||
authorizationToken,
|
||||
features: this._features,
|
||||
features: extractFeatures(),
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: null,
|
||||
serverId: serverId,
|
||||
@@ -300,7 +245,7 @@ export default class Main {
|
||||
masterKey,
|
||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
||||
authorizationToken: undefined,
|
||||
features: this._features,
|
||||
features: extractFeatures(),
|
||||
csmEndpoint: undefined,
|
||||
dnsSuffix: null,
|
||||
serverId: serverId,
|
||||
@@ -316,32 +261,6 @@ export default class Main {
|
||||
throw new Error(`Unsupported AuthType ${authType}`);
|
||||
}
|
||||
|
||||
private static _instantiateExplorer(): Explorer {
|
||||
const explorer = new Explorer();
|
||||
// workaround to resolve cyclic refs with view
|
||||
explorer.renewExplorerShareAccess = Main.renewExplorerAccess;
|
||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||
|
||||
// Hosted needs click to dismiss any menu
|
||||
if (window.authType === AuthType.AAD) {
|
||||
window.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
sendMessage({
|
||||
type: MessageTypes.ExplorerClickEvent
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
private static _hasCachedEncryptedKey(): boolean {
|
||||
return SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken);
|
||||
}
|
||||
|
||||
private static _getDatabaseAccountKindFromExperience(apiExperience: string): string {
|
||||
if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||
return Constants.AccountKind.MongoDB;
|
||||
@@ -354,19 +273,9 @@ export default class Main {
|
||||
return Constants.AccountKind.GlobalDocumentDB;
|
||||
}
|
||||
|
||||
private static _getAccessInputMetadata(accessInput: string): Q.Promise<void> {
|
||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
||||
AuthHeadersUtil.getAccessInputMetadata(accessInput).then(
|
||||
(metadata: any) => {
|
||||
Main._accessInputMetadata = metadata;
|
||||
deferred.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
private static async _getAccessInputMetadata(accessInput: string): Promise<void> {
|
||||
const metadata = await AuthHeadersUtil.getAccessInputMetadata(accessInput);
|
||||
Main._accessInputMetadata = metadata;
|
||||
}
|
||||
|
||||
private static _getMasterKeyFromConnectionString(connectionString: string): string {
|
||||
@@ -447,84 +356,4 @@ export default class Main {
|
||||
explorer.isAccountReady.valueHasMutated();
|
||||
sendMessage("ready");
|
||||
}
|
||||
|
||||
private static _shouldProcessMessage(event: MessageEvent): boolean {
|
||||
if (typeof event.data !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (event.data["signature"] !== "pcIframe") {
|
||||
return false;
|
||||
}
|
||||
if (!("data" in event.data)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof event.data["data"] !== "object") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _handleMessage(event: MessageEvent) {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._shouldProcessMessage(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message: any = event.data.data;
|
||||
if (message.type) {
|
||||
if (message.type === MessageTypes.GetAccessAadResponse && (message.response || message.error)) {
|
||||
if (message.response) {
|
||||
Main._handleGetAccessAadSucceed(message.response);
|
||||
}
|
||||
if (message.error) {
|
||||
Main._handleGetAccessAadFailed(message.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.type === MessageTypes.SwitchAccount && message.account && message.keys) {
|
||||
Main._handleSwitchAccountSucceed(message.account, message.keys, message.authorizationToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static _handleSwitchAccountSucceed(account: DatabaseAccount, keys: AccountKeys, authorizationToken: string) {
|
||||
if (!this._explorer) {
|
||||
console.error("no explorer found");
|
||||
return;
|
||||
}
|
||||
|
||||
this._explorer.hideConnectExplorerForm();
|
||||
|
||||
const masterKey = Main._getMasterKey(keys);
|
||||
this._explorer.notificationConsoleData([]);
|
||||
Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken);
|
||||
}
|
||||
|
||||
private static _handleGetAccessAadSucceed(response: [DatabaseAccount, AccountKeys, string]) {
|
||||
if (!response || response.length < 1) {
|
||||
return;
|
||||
}
|
||||
const account = response[0];
|
||||
const masterKey = Main._getMasterKey(response[1]);
|
||||
const authorizationToken = response[2];
|
||||
Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken);
|
||||
this._getAadAccessDeferred.resolve(this._explorer);
|
||||
}
|
||||
|
||||
private static _getMasterKey(keys: AccountKeys): string {
|
||||
return (
|
||||
keys?.primaryMasterKey ??
|
||||
keys?.secondaryMasterKey ??
|
||||
keys?.primaryReadonlyMasterKey ??
|
||||
keys?.secondaryReadonlyMasterKey
|
||||
);
|
||||
}
|
||||
|
||||
private static _handleGetAccessAadFailed(error: any) {
|
||||
this._getAadAccessDeferred.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
import Main from "./Main";
|
||||
|
||||
describe("Main", () => {
|
||||
it("correctly detects feature flags", () => {
|
||||
// Search containing non-features, with Camelcase keys and uri encoded values
|
||||
const params = new URLSearchParams(
|
||||
"?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey"
|
||||
);
|
||||
const features = Main.extractFeatures(params);
|
||||
|
||||
expect(features).toEqual({
|
||||
notebookserverurl: "https://localhost:10001/12345/notebook",
|
||||
notebookservertoken: "token",
|
||||
enablenotebooks: "true"
|
||||
});
|
||||
});
|
||||
|
||||
it("correctly parses resource token connection string", () => {
|
||||
const connectionString =
|
||||
"AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;";
|
||||
|
||||
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 * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
if (window.authType === AuthType.EncryptedToken) {
|
||||
@@ -21,19 +19,6 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet
|
||||
}
|
||||
}
|
||||
|
||||
export async function getArcadiaAuthToken(
|
||||
arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT,
|
||||
tenantId?: string
|
||||
): Promise<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) {
|
||||
if (!token) {
|
||||
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="App"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user