Initial Move from Azure DevOps to GitHub

This commit is contained in:
Steve Faulkner
2020-05-25 21:30:55 -05:00
commit 36581fb6d9
986 changed files with 195242 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
import Q from "q";
import { DataAccessUtilityBase } from "../../Common/DataAccessUtilityBase";
export class DataAccessUtility extends DataAccessUtilityBase {
public refreshCachedOffers(): Q.Promise<void> {
return Q();
}
public refreshCachedResources(options: any): Q.Promise<void> {
return Q();
}
}

View File

@@ -0,0 +1,39 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { AccountKind, TagNames, DefaultAccountExperience } from "../../Common/Constants";
import Explorer from "../../Explorer/Explorer";
import { NotificationsClient } from "./NotificationsClient";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import { DataAccessUtility } from "./DataAccessUtility";
export default class EmulatorExplorerFactory {
public static createExplorer(): ViewModels.Explorer {
DocumentClientUtilityBase;
const documentClientUtility: DocumentClientUtilityBase = new DocumentClientUtilityBase(new DataAccessUtility());
const explorer: Explorer = new Explorer({
documentClientUtility: documentClientUtility,
notificationsClient: new NotificationsClient(),
isEmulator: true
});
explorer.databaseAccount({
name: "",
id: "",
location: "",
type: "",
kind: AccountKind.DocumentDB,
tags: {
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
},
properties: {
documentEndpoint: "",
tableEndpoint: "",
gremlinEndpoint: "",
cassandraEndpoint: ""
}
});
explorer.isAccountReady(true);
return explorer;
}
}

View File

@@ -0,0 +1,7 @@
import * as ViewModels from "../../Contracts/ViewModels";
import EmulatorExplorerFactory from "./ExplorerFactory";
export function initializeExplorer(): ViewModels.Explorer {
const explorer = EmulatorExplorerFactory.createExplorer();
return explorer;
}

View File

@@ -0,0 +1,16 @@
import Q from "q";
import * as DataModels from "../../Contracts/DataModels";
import { NotificationsClientBase } from "../../Common/NotificationsClientBase";
export class NotificationsClient extends NotificationsClientBase {
private static readonly _notificationsApiSuffix: string = "/api/notifications";
public constructor() {
super(NotificationsClient._notificationsApiSuffix);
}
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
// no notifications for the emulator
return Q([]);
}
}

View File

@@ -0,0 +1,171 @@
import AuthHeadersUtil from "./Authorization";
import * as Constants from "../../Common/Constants";
import { Logger } from "../../Common/Logger";
import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels";
import { config } from "../../Config";
// TODO: 421864 - add a fetch wrapper
export abstract class ArmResourceUtils {
private static readonly _armEndpoint: string = config.ARM_ENDPOINT;
private static readonly _armApiVersion: string = config.ARM_API_VERSION;
private static readonly _armAuthArea: string = config.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(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(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(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 url = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/listKeys?api-version=${Constants.ArmApiVersions.documentDB}`;
const response: Response = await fetch(url, { headers: fetchHeaders, method: "POST" });
const result: AccountKeys = response.status === 204 || response.status === 304 ? null : await response.json();
if (!response.ok) {
throw result;
}
return result;
} catch (error) {
Logger.logError(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(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[];
}

View File

@@ -0,0 +1,310 @@
import "expose-loader?AuthenticationContext!../../../externals/adal";
import Q from "q";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { AuthType } from "../../AuthType";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { CosmosClient } from "../../Common/CosmosClient";
import { Logger } from "../../Common/Logger";
import { config } from "../../Config";
export default class AuthHeadersUtil {
// TODO: Figure out a way to determine the extension endpoint and serverId at runtime
public static extensionEndpoint: string = Constants.BackendEndpoints.productionPortal;
public static serverId: string = Constants.ServerIds.productionPortal;
private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d";
private static readonly _aadEndpoint: string = config.AAD_ENDPOINT;
private static readonly _armEndpoint: string = config.ARM_ENDPOINT;
private static readonly _arcadiaEndpoint: string = config.ARCADIA_ENDPOINT;
private static readonly _armAuthArea: string = config.ARM_AUTH_AREA;
private static readonly _graphEndpoint: string = config.GRAPH_ENDPOINT;
private static readonly _graphApiVersion: string = config.GRAPH_API_VERSION;
private static _authContext: AuthenticationContext = new AuthenticationContext({
instance: AuthHeadersUtil._aadEndpoint,
clientId: AuthHeadersUtil._firstPartyAppId,
postLogoutRedirectUri: window.location.origin,
endpoints: {
aad: AuthHeadersUtil._aadEndpoint,
graph: AuthHeadersUtil._graphEndpoint,
armAuthArea: AuthHeadersUtil._armAuthArea,
armEndpoint: AuthHeadersUtil._armEndpoint,
arcadiaEndpoint: AuthHeadersUtil._arcadiaEndpoint
},
tenant: undefined,
cacheLocation: window.navigator.userAgent.indexOf("Edge") > -1 ? "localStorage" : undefined
});
public static getAccessInputMetadata(accessInput: string): Q.Promise<DataModels.AccessInputMetadata> {
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>();
const url: string = `${AuthHeadersUtil.extensionEndpoint}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`;
const authType: string = (<any>window).authType;
const headers: { [headerName: string]: string } = {};
if (authType === AuthType.EncryptedToken) {
headers[Constants.HttpHeaders.guestAccessToken] = accessInput;
} else {
headers[Constants.HttpHeaders.connectionString] = accessInput;
}
$.ajax({
url: url,
type: "GET",
headers: headers,
cache: false,
dataType: "text"
}).then(
(data: string, textStatus: string, xhr: JQueryXHR<any>) => {
if (!data) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to get access input metadata`);
deferred.reject(`Failed to get access input metadata`);
}
try {
const metadata: DataModels.AccessInputMetadata = JSON.parse(JSON.parse(data));
deferred.resolve(metadata); // TODO: update to a single JSON parse once backend response is stringified exactly once
} catch (error) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to parse access input metadata");
deferred.reject("Failed to parse access input metadata");
throw error;
}
},
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while fetching access input metadata: ${JSON.stringify(xhr.responseText)}`
);
deferred.reject(xhr.responseText);
}
);
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
}
public static generateEncryptedToken(): Q.Promise<DataModels.GenerateTokenResponse> {
const url: string = `${
AuthHeadersUtil.extensionEndpoint
}/api/tokens/generateToken${AuthHeadersUtil._generateResourceUrl()}`;
const explorer: ViewModels.Explorer = (<any>window).dataExplorer;
const headers: any = { authorization: CosmosClient.authorizationToken() };
headers[Constants.HttpHeaders.getReadOnlyKey] = !explorer.hasWriteAccess();
return AuthHeadersUtil._initiateGenerateTokenRequest({
url: url,
type: "POST",
headers: headers,
contentType: "application/json",
cache: false
});
}
public static generateUnauthenticatedEncryptedTokenForConnectionString(
connectionString: string
): Q.Promise<DataModels.GenerateTokenResponse> {
if (!connectionString) {
return Q.reject("None or empty connection string specified");
}
const url: string = `${AuthHeadersUtil.extensionEndpoint}/api/guest/tokens/generateToken`;
const headers: any = {};
headers[Constants.HttpHeaders.connectionString] = connectionString;
return AuthHeadersUtil._initiateGenerateTokenRequest({
url: url,
type: "POST",
headers: headers,
contentType: "application/json",
cache: false
});
}
public static isUserSignedIn(): boolean {
const user = AuthHeadersUtil._authContext.getCachedUser();
return !!user;
}
public static getCachedUser(): AuthenticationContext.UserInfo {
if (this.isUserSignedIn()) {
return AuthHeadersUtil._authContext.getCachedUser();
}
return undefined;
}
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: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
const subscriptionId: string = CosmosClient.subscriptionId();
const resourceGroup: string = CosmosClient.resourceGroup();
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount);
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience);
const accountEndpoint = (databaseAccount && databaseAccount.properties.documentEndpoint) || "";
const sid = subscriptionId || "";
const rg = resourceGroup || "";
const dba = (databaseAccount && databaseAccount.name) || "";
const resourceUrl = encodeURIComponent(accountEndpoint);
const rid = "";
const rtype = "";
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
}
private static _initiateGenerateTokenRequest(
requestSettings: JQueryAjaxSettings<any>
): Q.Promise<DataModels.GenerateTokenResponse> {
const deferred: Q.Deferred<DataModels.GenerateTokenResponse> = Q.defer<DataModels.GenerateTokenResponse>();
$.ajax(requestSettings).then(
(data: string, textStatus: string, xhr: JQueryXHR<any>) => {
if (!data) {
deferred.reject("No token generated");
}
deferred.resolve(JSON.parse(data));
},
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
deferred.reject(xhr.responseText);
}
);
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
}
}

View File

@@ -0,0 +1,12 @@
import Q from "q";
import { DataAccessUtilityBase } from "../../Common/DataAccessUtilityBase";
export class DataAccessUtility extends DataAccessUtilityBase {
public refreshCachedOffers(): Q.Promise<void> {
return Q();
}
public refreshCachedResources(): Q.Promise<void> {
return Q();
}
}

View File

@@ -0,0 +1,27 @@
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../../Explorer/Explorer";
import { NotificationsClient } from "./NotificationsClient";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import { DataAccessUtility } from "./DataAccessUtility";
export default class HostedExplorerFactory {
public createExplorer(): ViewModels.Explorer {
var documentClientUtility = new DocumentClientUtilityBase(new DataAccessUtility());
const explorer = new Explorer({
documentClientUtility: documentClientUtility,
notificationsClient: new NotificationsClient(),
isEmulator: false
});
return explorer;
}
public static reInitializeDocumentClientUtilityForExplorer(explorer: ViewModels.Explorer): void {
if (!!explorer) {
const documentClientUtility = new DocumentClientUtilityBase(new DataAccessUtility());
explorer.rebindDocumentClientUtility(documentClientUtility);
explorer.notificationConsoleData([]);
}
}
}

View File

@@ -0,0 +1,75 @@
import * as DataModels from "../../../Contracts/DataModels";
import { ConnectionStringParser } from "./ConnectionStringParser";
describe("ConnectionStringParser", () => {
const mockAccountName: string = "Test";
const mockMasterKey: string = "some-key";
it("should parse a valid sql account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};`
);
expect(metadata.accountName).toBe(mockAccountName);
expect(metadata.apiKind).toBe(DataModels.ApiKind.SQL);
});
it("should parse a valid mongo account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255`
);
expect(metadata.accountName).toBe(mockAccountName);
expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDB);
});
it("should parse a valid compute mongo account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255`
);
expect(metadata.accountName).toBe(mockAccountName);
expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDBCompute);
});
it("should parse a valid graph account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;`
);
expect(metadata.accountName).toBe(mockAccountName);
expect(metadata.apiKind).toBe(DataModels.ApiKind.Graph);
});
it("should parse a valid table account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
`DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`
);
expect(metadata.accountName).toBe(mockAccountName);
expect(metadata.apiKind).toBe(DataModels.ApiKind.Table);
});
it("should parse a valid cassandra account connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
`AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};`
);
expect(metadata.accountName).toBe(mockAccountName);
expect(metadata.apiKind).toBe(DataModels.ApiKind.Cassandra);
});
it("should fail to parse an invalid connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
"some-rogue-connection-string"
);
expect(metadata).toBe(undefined);
});
it("should fail to parse an empty connection string", () => {
const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString("");
expect(metadata).toBe(undefined);
});
});

View File

@@ -0,0 +1,46 @@
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
export class ConnectionStringParser {
public static parseConnectionString(connectionString: string): DataModels.AccessInputMetadata {
if (!!connectionString) {
try {
const accessInput: DataModels.AccessInputMetadata = {} as DataModels.AccessInputMetadata;
const connectionStringParts = connectionString.split(";");
connectionStringParts.forEach((connectionStringPart: string) => {
if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) {
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1];
accessInput.apiKind = DataModels.ApiKind.SQL;
} else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) {
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo);
accessInput.accountName = matches && matches.length > 1 && matches[2];
accessInput.apiKind = DataModels.ApiKind.MongoDB;
} else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) {
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
accessInput.accountName = matches && matches.length > 1 && matches[2];
accessInput.apiKind = DataModels.ApiKind.MongoDBCompute;
} else if (RegExp(Constants.EndpointsRegex.cassandra).test(connectionStringPart)) {
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.cassandra)[1];
accessInput.apiKind = DataModels.ApiKind.Cassandra;
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
accessInput.apiKind = DataModels.ApiKind.Table;
} else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) {
accessInput.apiKind = DataModels.ApiKind.Graph;
}
});
if (Object.keys(accessInput).length === 0) {
return undefined;
}
return accessInput;
} catch (error) {
return undefined;
}
}
return undefined;
}
}

View File

@@ -0,0 +1,32 @@
import { AccessInputMetadata } from "../../Contracts/DataModels";
import { HostedUtils } from "./HostedUtils";
describe("getDatabaseAccountPropertiesFromMetadata", () => {
it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => {
let mongoComputeAccount: AccessInputMetadata = {
accountName: "compute-batch2",
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
apiKind: 5,
documentEndpoint: "https://compute-batch2.documents.azure.com:443/",
expiryTimestamp: "1234",
mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/"
};
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({
mongoEndpoint: mongoComputeAccount.mongoEndpoint,
documentEndpoint: mongoComputeAccount.documentEndpoint
});
});
it("should not return an object with the mongoEndpoint key if the apiKind is mongo (1)", () => {
let mongoAccount: AccessInputMetadata = {
accountName: "compute-batch2",
apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255",
apiKind: 1,
documentEndpoint: "https://compute-batch2.documents.azure.com:443/",
expiryTimestamp: "1234"
};
expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({
documentEndpoint: mongoAccount.documentEndpoint
});
});
});

View File

@@ -0,0 +1,35 @@
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import { AccessInputMetadata } from "../../Contracts/DataModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
export class HostedUtils {
static getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): any {
let properties = { documentEndpoint: metadata.documentEndpoint };
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind);
if (apiExperience === Constants.DefaultAccountExperience.Cassandra) {
properties = Object.assign(properties, {
cassandraEndpoint: metadata.apiEndpoint,
capabilities: [{ name: Constants.CapabilityNames.EnableCassandra }]
});
} else if (apiExperience === Constants.DefaultAccountExperience.Table) {
properties = Object.assign(properties, {
tableEndpoint: metadata.apiEndpoint,
capabilities: [{ name: Constants.CapabilityNames.EnableTable }]
});
} else if (apiExperience === Constants.DefaultAccountExperience.Graph) {
properties = Object.assign(properties, {
gremlinEndpoint: metadata.apiEndpoint,
capabilities: [{ name: Constants.CapabilityNames.EnableGremlin }]
});
} else if (apiExperience === Constants.DefaultAccountExperience.MongoDB) {
if (metadata.apiKind === DataModels.ApiKind.MongoDBCompute) {
properties = Object.assign(properties, {
mongoEndpoint: metadata.mongoEndpoint
});
}
}
return properties;
}
}

588
src/Platform/Hosted/Main.ts Normal file
View File

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

View File

@@ -0,0 +1,45 @@
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;";
const properties = Main.parseResourceTokenConnectionString(connectionString);
expect(properties).toEqual({
accountEndpoint: "fakeEndpoint",
collectionId: "fakeCollectionId",
databaseId: "fakeDatabaseId",
partitionKey: undefined,
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"
});
});
it("correctly parses resource token connection string with partition key", () => {
const connectionString =
"type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;PartitionKey=fakePartitionKey;";
const properties = Main.parseResourceTokenConnectionString(connectionString);
expect(properties).toEqual({
accountEndpoint: "fakeEndpoint",
collectionId: "fakeCollectionId",
databaseId: "fakeDatabaseId",
partitionKey: "fakePartitionKey",
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"
});
});
});

View File

@@ -0,0 +1,9 @@
import { NotificationsClientBase } from "../../Common/NotificationsClientBase";
export class NotificationsClient extends NotificationsClientBase {
private static readonly _notificationsApiSuffix: string = "/api/guest/notifications";
public constructor() {
super(NotificationsClient._notificationsApiSuffix);
}
}

View File

@@ -0,0 +1,96 @@
import "jquery";
import * as _ from "underscore";
import Q from "q";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { DataAccessUtilityBase } from "../../Common/DataAccessUtilityBase";
import { MessageHandler } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
export class DataAccessUtility extends DataAccessUtilityBase {
public readDatabases(options: any): Q.Promise<DataModels.Database[]> {
return MessageHandler.sendCachedDataMessage<DataModels.Database[]>(MessageTypes.AllDatabases, [
(<any>window).dataExplorer.databaseAccount().id,
Constants.ClientDefaults.portalCacheTimeoutMs
]).catch(error => {
return super.readDatabases(options);
});
}
// public readCollections(database: ViewModels.Database, options: any): Q.Promise<DataModels.Collection[]> {
// return MessageHandler.sendCachedDataMessage<DataModels.Collection[]>(MessageTypes.CollectionsForDatabase, [
// (<any>window).dataExplorer.databaseAccount().id,
// database.id()
// ]);
// }
public readOffers(options: any): Q.Promise<DataModels.Offer[]> {
return MessageHandler.sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
(<any>window).dataExplorer.databaseAccount().id,
Constants.ClientDefaults.portalCacheTimeoutMs
]).catch(error => {
return super.readOffers(options);
});
}
public readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise<DataModels.OfferWithHeaders> {
const deferred: Q.Deferred<DataModels.OfferWithHeaders> = Q.defer<DataModels.OfferWithHeaders>();
super.readOffer(requestedResource, options).then(
(offer: DataModels.OfferWithHeaders) => deferred.resolve(offer),
(reason: any) => {
const isThrottled: boolean =
!!reason &&
!!reason.error &&
!!reason.error.code &&
reason.error.code == Constants.HttpStatusCodes.TooManyRequests;
if (isThrottled && MessageHandler.canSendMessage()) {
MessageHandler.sendCachedDataMessage<DataModels.OfferWithHeaders>(MessageTypes.SingleOffer, [
(<any>window).dataExplorer.databaseAccount().id,
requestedResource._self,
requestedResource.offerVersion
]).then(
(offer: DataModels.OfferWithHeaders) => deferred.resolve(offer),
(reason: any) => deferred.reject(reason)
);
return;
}
deferred.reject(reason);
}
);
return deferred.promise;
}
public updateOfferThroughputBeyondLimit(
updateThroughputRequestPayload: DataModels.UpdateOfferThroughputRequest,
options: any
): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>();
const explorer: ViewModels.Explorer = (<any>window).dataExplorer;
const url: string = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const requestOptions: any = _.extend({}, options, {});
requestOptions[authorizationHeader.header] = authorizationHeader.token;
const requestSettings: JQueryAjaxSettings<any> = {
type: "POST",
contentType: "application/json",
headers: requestOptions,
data: JSON.stringify(updateThroughputRequestPayload)
};
$.ajax(url, requestSettings).then(
(data: any, textStatus: string, xhr: JQueryXHR<any>) => {
deferred.resolve();
},
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
deferred.reject(xhr.responseText);
}
);
return deferred.promise;
}
}

View File

@@ -0,0 +1,20 @@
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../../Explorer/Explorer";
import { NotificationsClient } from "./NotificationsClient";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import { DataAccessUtility } from "./DataAccessUtility";
export default class PortalExplorerFactory {
public createExplorer(): ViewModels.Explorer {
var documentClientUtility = new DocumentClientUtilityBase(new DataAccessUtility());
var explorer = new Explorer({
documentClientUtility: documentClientUtility,
notificationsClient: new NotificationsClient(),
isEmulator: false
});
return explorer;
}
}

View File

@@ -0,0 +1,11 @@
import * as ViewModels from "../../Contracts/ViewModels";
import PortalExplorerFactory from "./ExplorerFactory";
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
export function initializeExplorer(): ViewModels.Explorer {
const portalExplorerFactory = new PortalExplorerFactory();
const explorer = portalExplorerFactory.createExplorer();
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
return explorer;
}

View File

@@ -0,0 +1,9 @@
import { NotificationsClientBase } from "../../Common/NotificationsClientBase";
export class NotificationsClient extends NotificationsClientBase {
private static readonly _notificationsApiSuffix: string = "/api/notifications";
public constructor() {
super(NotificationsClient._notificationsApiSuffix);
}
}