From 5235a1d0293edba641b59af38698efc73a93b587 Mon Sep 17 00:00:00 2001 From: Amir Omidi Date: Wed, 27 Nov 2019 12:33:08 -0800 Subject: [PATCH] New azure authentication experience (#8483) * Changes * Work in progress * Authenticate with azure * enbable national clouds and initialization * Add support for tenants * Finish up account work * Finish up azure auth * Don't allow prompt if we're not initialized * Shut down server * Remove trailing comma * encode uri component * ignore errors * Address comments on github * Fix issues and disable feature without env var * Don't encode the nonce * Only use env variables to disable the new sign in * Show more user friendly messages to users --- extensions/azurecore/package.json | 3 +- extensions/azurecore/package.nls.json | 5 +- .../account-provider/azureAccountProvider.ts | 1 + .../account-provider/azureAccountProvider2.ts | 448 ++++++++++++++++++ .../azureAccountProviderService.ts | 23 +- .../src/account-provider/interfaces.ts | 3 +- .../src/account-provider/providerSettings.ts | 55 +-- .../src/account-provider/tokenCache.ts | 24 +- extensions/azurecore/src/typings/ref.d.ts | 1 + extensions/azurecore/yarn.lock | 26 +- extensions/package.json | 2 +- extensions/yarn.lock | 8 +- .../contrib/accounts/browser/accountDialog.ts | 1 + 13 files changed, 531 insertions(+), 69 deletions(-) create mode 100644 extensions/azurecore/src/account-provider/azureAccountProvider2.ts diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index 3f3dcab138..281dd8672d 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -12,6 +12,7 @@ "activationEvents": [ "*" ], + "enableProposedApi": true, "main": "./out/extension", "repository": { "type": "git", @@ -179,7 +180,7 @@ "dependencies": { "@azure/arm-resourcegraph": "^2.0.0", "@azure/arm-subscriptions": "1.0.0", - "adal-node": "^0.1.28", + "adal-node": "^0.2.1", "request": "2.88.0", "vscode-nls": "^4.0.0" }, diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json index 418d2cfc86..00cf99420a 100644 --- a/extensions/azurecore/package.nls.json +++ b/extensions/azurecore/package.nls.json @@ -18,5 +18,6 @@ "config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled", "config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled", "config.enableChinaCloudDescription": "Should Azure China integration be enabled", - "config.enableGermanyCloudDescription": "Should Azure Germany integration be enabled" -} \ No newline at end of file + "config.enableGermanyCloudDescription": "Should Azure Germany integration be enabled", + "config.useNewSignInExperience": "Use the new Azure sign in experience" +} diff --git a/extensions/azurecore/src/account-provider/azureAccountProvider.ts b/extensions/azurecore/src/account-provider/azureAccountProvider.ts index 1bc8107ec1..100e161531 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProvider.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProvider.ts @@ -432,6 +432,7 @@ export class AzureAccountProvider implements azdata.AccountProvider { displayName: displayName }, properties: { + providerSettings: this._metadata, isMsAccount: msa, tenants: tenants }, diff --git a/extensions/azurecore/src/account-provider/azureAccountProvider2.ts b/extensions/azurecore/src/account-provider/azureAccountProvider2.ts new file mode 100644 index 0000000000..9f95e2c88f --- /dev/null +++ b/extensions/azurecore/src/account-provider/azureAccountProvider2.ts @@ -0,0 +1,448 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as http from 'http'; +import * as url from 'url'; +import * as crypto from 'crypto'; +import * as nls from 'vscode-nls'; +import * as request from 'request'; +import { + AzureAccount, + AzureAccountProviderMetadata, + AzureAccountSecurityTokenCollection, + AzureAccountSecurityToken, + Tenant, +} from './interfaces'; + +import TokenCache from './tokenCache'; +import { AddressInfo } from 'net'; +import { AuthenticationContext, TokenResponse, ErrorResponse } from 'adal-node'; +import { promisify } from 'util'; +import * as events from 'events'; + +const localize = nls.loadMessageBundle(); +const notInitalizedMessage = localize('accountProviderNotInitialized', "Account provider not initialized, cannot perform action"); + +export class AzureAccountProvider implements azdata.AccountProvider { + private static AzureAccountAuthenticatedEvent: string = 'AzureAccountAuthenticated'; + private static WorkSchoolAccountType: string = 'work_school'; + private static MicrosoftAccountType: string = 'microsoft'; + private static AadCommonTenant: string = 'common'; + + private static eventEmitter = new events.EventEmitter(); + private static redirectUrlAAD = 'https://vscode-redirect.azurewebsites.net/'; + private commonAuthorityUrl: string; + private isInitialized: boolean = false; + + + constructor(private metadata: AzureAccountProviderMetadata, private _tokenCache: TokenCache) { + this.commonAuthorityUrl = url.resolve(this.metadata.settings.host, AzureAccountProvider.AadCommonTenant); + // Temporary override + this.metadata.settings.clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56'; + } + + // interface method + initialize(storedAccounts: azdata.Account[]): Thenable { + return this._initialize(storedAccounts); + } + + private async _initialize(storedAccounts: azdata.Account[]): Promise { + for (let account of storedAccounts) { + try { + await this.getAccessTokens(account, azdata.AzureResource.ResourceManagement); + } catch (e) { + console.error(`Refreshing account ${account.displayInfo} failed - ${e}`); + account.isStale = true; + azdata.accounts.accountUpdated(account); + } + } + this.isInitialized = true; + return storedAccounts; + } + + private async getToken(userId: string, tenantId: string, resourceId: string): Promise { + let authorityUrl = url.resolve(this.metadata.settings.host, tenantId); + const context = new AuthenticationContext(authorityUrl, null, this._tokenCache); + + const acquireToken = promisify(context.acquireToken).bind(context); + + let response: (TokenResponse | ErrorResponse) = await acquireToken(resourceId, userId, this.metadata.settings.clientId); + if (response.error) { + throw new Error(`Response contained error ${response}`); + } + + response = response as TokenResponse; + + context.cache.add([response], (err, result) => { + console.log(err, result); + }); + + return response; + } + + private async getAccessTokens(account: azdata.Account, resource: azdata.AzureResource): Promise { + const resourceIdMap = new Map([ + [azdata.AzureResource.ResourceManagement, this.metadata.settings.armResource.id], + [azdata.AzureResource.Sql, this.metadata.settings.sqlResource.id] + ]); + const tenantRefreshPromises: Promise<{ tenantId: any, securityToken: AzureAccountSecurityToken }>[] = []; + const tokenCollection: AzureAccountSecurityTokenCollection = {}; + + for (let tenant of account.properties.tenants) { + const promise = new Promise<{ tenantId: any, securityToken: AzureAccountSecurityToken }>(async (resolve, reject) => { + try { + let response = await this.getToken(tenant.userId, tenant.id, resourceIdMap.get(resource)); + + resolve({ + tenantId: tenant.id, + securityToken: { + expiresOn: response.expiresOn, + resource: response.resource, + token: response.accessToken, + tokenType: response.tokenType + } as AzureAccountSecurityToken, + }); + } catch (ex) { + reject(ex); + } + + }); + tenantRefreshPromises.push(promise); + } + + const refreshedTenants = await Promise.all(tenantRefreshPromises); + refreshedTenants.forEach((refreshed) => { + tokenCollection[refreshed.tenantId] = refreshed.securityToken; + }); + + return tokenCollection; + } + + // interface method + getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{}> { + return this._getSecurityToken(account, resource); + } + + private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise<{}> { + return this.getAccessTokens(account, resource); + } + + // interface method + prompt(): Thenable { + return this._prompt(); + } + + + private async _prompt(): Promise { + if (this.isInitialized === false) { + vscode.window.showInformationMessage(notInitalizedMessage); + return { canceled: false }; + } + const pathMappings = new Map void>(); + + const nonce = crypto.randomBytes(16).toString('base64'); + + const server = this.createAuthServer(pathMappings); + + const port = await this.listenToServer(server); + try { + const authUrl = this.createAuthUrl( + this.metadata.settings.host, + AzureAccountProvider.redirectUrlAAD, + this.metadata.settings.clientId, + this.metadata.settings.signInResourceId, + AzureAccountProvider.AadCommonTenant, + `${port},${encodeURIComponent(nonce)}` + ); + + this.addServerPaths(pathMappings, nonce, authUrl); + + const accountAuthenticatedPromise = new Promise((resolve, reject) => { + AzureAccountProvider.eventEmitter.on(AzureAccountProvider.AzureAccountAuthenticatedEvent, ({ account, error }) => { + if (error) { + return reject(error); + } + return resolve(account); + }); + }); + + const urlToOpen = `http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`; + + vscode.env.openExternal(vscode.Uri.parse(urlToOpen)); + + const account = await accountAuthenticatedPromise; + + return account; + } finally { + server.close(); + } + } + + private addServerPaths( + pathMappings: Map void>, + nonce: string, + authUrl: string) { + + const initialSignIn = ((req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => { + const receivedNonce = (reqUrl.query.nonce as string || '').replace(/ /g, '+'); + if (receivedNonce !== nonce) { + res.writeHead(400, { 'content-type': 'text/html' }); + res.write(localize('azureAuth.nonceError', "Authentication failed due to a nonce mismatch, please close ADS and try again.")); + res.end(); + return; + } + res.writeHead(302, { Location: authUrl }); + res.end(); + }); + + const callback = ((req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => { + const state = reqUrl.query.state as string ?? ''; + const code = reqUrl.query.code as string ?? ''; + + const stateSplit = state.split(','); + if (stateSplit.length !== 2) { + res.writeHead(400, { 'content-type': 'text/html' }); + res.write(localize('azureAuth.stateError', "Authentication failed due to a state mismatch, please close ADS and try again.")); + res.end(); + return; + } + + if (stateSplit[1] !== nonce) { + res.writeHead(400, { 'content-type': 'text/html' }); + res.write(localize('azureAuth.nonceError', "Authentication failed due to a nonce mismatch, please close ADS and try again.")); + res.end(); + return; + } + + res.writeHead(200, { 'content-type': 'text/html' }); + res.write(localize('azureAuth.authSuccessful', "Authentication was successful, you can now close this page.")); + res.end(); + + this.handleAuthentication(code).catch(console.error); + }); + + pathMappings.set('/signin', initialSignIn); + pathMappings.set('/callback', callback); + } + private async makeWebRequest(accessToken: TokenResponse, uri: string): Promise { + const params = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken.accessToken}` + }, + json: true + }; + + return new Promise((resolve, reject) => { + request.get(uri, params, (error: any, response: request.Response, body: any) => { + const err = error ?? body.error; + if (err) { + return reject(err); + } + return resolve(body.value); + }); + }); + } + + private async getTenants(userId: string, homeTenant: string): Promise { + const armToken = await this.getToken(userId, AzureAccountProvider.AadCommonTenant, this.metadata.settings.armResource.id); + const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2015-01-01'); + const armWebResponse: any[] = await this.makeWebRequest(armToken, tenantUri); + + const promises = armWebResponse.map(async (value: { tenantId: string }) => { + const graphToken = await this.getToken(userId, value.tenantId, this.metadata.settings.graphResource.id); + let tenantDetailsUri = url.resolve(this.metadata.settings.graphResource.endpoint, value.tenantId + '/'); + tenantDetailsUri = url.resolve(tenantDetailsUri, 'tenantDetails?api-version=2013-04-05'); + const tenantDetails: any[] = await this.makeWebRequest(graphToken, tenantDetailsUri); + + return { + id: value.tenantId, + userId: userId, + displayName: tenantDetailsUri.length && tenantDetails[0].displayName + ? tenantDetails[0].displayName + : localize('azureWorkAccountDisplayName', "Work or school account") + } as Tenant; + }); + + const tenants = await Promise.all(promises); + const homeTenantIndex = tenants.findIndex(tenant => tenant.id === homeTenant); + if (homeTenantIndex >= 0) { + const homeTenant = tenants.splice(homeTenantIndex, 1); + tenants.unshift(homeTenant[0]); + } + return tenants; + } + + /** + * Authenticates an azure account and then emits an event + * @param code Code from authenticating + */ + private async handleAuthentication(code: string): Promise { + const token = await this.getTokenWithAuthCode(code, AzureAccountProvider.redirectUrlAAD); + + const tenants = await this.getTenants(token.userId, token.userId); + let identityProvider = token.identityProvider; + if (identityProvider) { + identityProvider = identityProvider.toLowerCase(); + } + + // Determine if this is a microsoft account + let msa = identityProvider && ( + identityProvider.indexOf('live.com') !== -1 || // lgtm [js/incomplete-url-substring-sanitization] + identityProvider.indexOf('live-int.com') !== -1 || // lgtm [js/incomplete-url-substring-sanitization] + identityProvider.indexOf('f8cdef31-a31e-4b4a-93e4-5f571e91255a') !== -1 || + identityProvider.indexOf('ea8a4392-515e-481f-879e-6571ff2a8a36') !== -1); + + // Calculate the display name for the user + let displayName = (token.givenName && token.familyName) + ? `${token.givenName} ${token.familyName}` + : token.userId; + + // Calculate the home tenant display name to use for the contextual display name + let contextualDisplayName = msa + ? localize('microsoftAccountDisplayName', "Microsoft Account") + : tenants[0].displayName; + + let accountType = msa + ? AzureAccountProvider.MicrosoftAccountType + : AzureAccountProvider.WorkSchoolAccountType; + + const account = { + key: { + providerId: this.metadata.id, + accountId: token.userId + }, + name: token.userId, + displayInfo: { + accountType: accountType, + userId: token.userId, + contextualDisplayName: contextualDisplayName, + displayName: displayName + }, + properties: { + providerSettings: this.metadata, + isMsAccount: msa, + tenants, + }, + isStale: false + } as AzureAccount; + + AzureAccountProvider.eventEmitter.emit(AzureAccountProvider.AzureAccountAuthenticatedEvent, { account }); + } + + private async getTokenWithAuthCode(code: string, redirectUrl: string): Promise { + const context = new AuthenticationContext(this.commonAuthorityUrl, null, this._tokenCache); + const acquireToken = promisify(context.acquireTokenWithAuthorizationCode).bind(context); + + let token = await acquireToken(code, redirectUrl, this.metadata.settings.signInResourceId, this.metadata.settings.clientId, undefined); + if (token.error) { + throw new Error(`${token.error} - ${token.errorDescription}`); + } + token = token as TokenResponse; + token._clientId = this.metadata.settings.clientId; + token._authority = this.commonAuthorityUrl; + token.isMRRT = true; + + context.cache.add([token], (err, result) => { + console.log(err, result); + }); + + return token; + } + + private createAuthUrl(baseHost: string, redirectUri: string, clientId: string, resource: string, tenant: string, nonce: string): string { + return `${baseHost}${encodeURIComponent(tenant)}/oauth2/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${nonce}&resource=${encodeURIComponent(resource)}&prompt=select_account`; + } + + private createAuthServer(pathMappings: Map void>) { + const server = http.createServer((req, res) => { + // Parse URL and the query string + const reqUrl = url.parse(req.url, true); + + const method = pathMappings.get(reqUrl.pathname); + if (method) { + method(req, res, reqUrl); + } else { + console.error('undefined request ', reqUrl, req); + } + }); + + return server; + } + + /** + * Actually starts listening for the server - returns the port the server is listening on + * @param server http.Server + */ + private async listenToServer(server: http.Server): Promise { + let portTimer: NodeJS.Timer; + const cancelPortTimer = (() => { + clearTimeout(portTimer); + }); + + const port = new Promise((resolve, reject) => { + // If no port for 5 seconds, reject it. + portTimer = setTimeout(() => { + reject(new Error('Timeout waiting for port')); + }, 5000); + + server.on('listening', () => { + const address = server.address() as AddressInfo; + if (address!.port === undefined) { + reject(new Error('Port was not defined')); + } + resolve(address.port); + }); + server.on('error', err => { + reject(err); + }); + server.on('close', () => { + reject(new Error('Closed')); + }); + server.listen(0, '127.0.0.1'); + }); + + const portValue = await port; + cancelPortTimer(); + + return portValue; + } + + // interface method + refresh(account: azdata.Account): Thenable { + return this._refresh(account); + } + + private async _refresh(account: azdata.Account): Promise { + return this.prompt(); + } + + // interface method + clear(accountKey: azdata.AccountKey): Thenable { + return this._clear(accountKey); + } + + private async _clear(accountKey: azdata.AccountKey): Promise { + // Put together a query to look up any tokens associated with the account key + let query = { userId: accountKey.accountId } as TokenResponse; + + // 1) Look up the tokens associated with the query + // 2) Remove them + let results = await this._tokenCache.findThenable(query); + this._tokenCache.removeThenable(results); + } + + // interface method + autoOAuthCancelled(): Thenable { + return this._autoOAuthCancelled(); + } + + private async _autoOAuthCancelled(): Promise { + // I don't think we need this? + throw new Error('Method not implemented.'); + } +} diff --git a/extensions/azurecore/src/account-provider/azureAccountProviderService.ts b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts index 1ab570ff55..08a12b9aa5 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProviderService.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as constants from '../constants'; import * as azdata from 'azdata'; import * as events from 'events'; @@ -13,7 +11,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import CredentialServiceTokenCache from './tokenCache'; import providerSettings from './providerSettings'; -import { AzureAccountProvider } from './azureAccountProvider'; +import { AzureAccountProvider as AzureAccountProviderDeprecated } from './azureAccountProvider'; +import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider2'; import { AzureAccountProviderMetadata, ProviderSettings } from './interfaces'; let localize = nls.loadMessageBundle(); @@ -26,7 +25,7 @@ export class AzureAccountProviderService implements vscode.Disposable { // MEMBER VARIABLES //////////////////////////////////////////////////////// private _accountDisposals: { [accountProviderId: string]: vscode.Disposable }; - private _accountProviders: { [accountProviderId: string]: AzureAccountProvider }; + private _accountProviders: { [accountProviderId: string]: azdata.AccountProvider }; private _credentialProvider: azdata.CredentialProvider; private _configChangePromiseChain: Thenable; private _currentConfig: vscode.WorkspaceConfiguration; @@ -69,10 +68,11 @@ export class AzureAccountProviderService implements vscode.Disposable { // PRIVATE HELPERS ///////////////////////////////////////////////////// private onClearTokenCache(): Thenable { - let self = this; + // let self = this; let promises: Thenable[] = providerSettings.map(provider => { - return self._accountProviders[provider.metadata.id].clearTokenCache(); + // return self._accountProviders[provider.metadata.id].clearTokenCache(); + return Promise.resolve(); }); return Promise.all(promises) @@ -129,14 +129,23 @@ export class AzureAccountProviderService implements vscode.Disposable { } private registerAccountProvider(provider: ProviderSettings): Thenable { + let self = this; return new Promise((resolve, reject) => { try { + //let config = vscode.workspace.getConfiguration(AzureAccountProviderService.ConfigurationSection); + let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`; let tokenCachePath = path.join(this._userStoragePath, tokenCacheKey); let tokenCache = new CredentialServiceTokenCache(self._credentialProvider, tokenCacheKey, tokenCachePath); - let accountProvider = new AzureAccountProvider(provider.metadata, tokenCache); + let accountProvider: azdata.AccountProvider; + + if (/*config.get('useNewSignInExperience') === true && */ Boolean(process.env['NEW_SIGN_IN_EXPERIENCE']) === true) { + accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata, tokenCache); + } else { + accountProvider = new AzureAccountProviderDeprecated(provider.metadata as AzureAccountProviderMetadata, tokenCache); + } self._accountProviders[provider.metadata.id] = accountProvider; self._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider); resolve(); diff --git a/extensions/azurecore/src/account-provider/interfaces.ts b/extensions/azurecore/src/account-provider/interfaces.ts index 9874554aff..20bed6c987 100644 --- a/extensions/azurecore/src/account-provider/interfaces.ts +++ b/extensions/azurecore/src/account-provider/interfaces.ts @@ -127,6 +127,7 @@ export interface AzureAccountProviderMetadata extends azdata.AccountProviderMeta * Properties specific to an Azure account */ interface AzureAccountProperties { + providerSettings: AzureAccountProviderMetadata; /** * Whether or not the account is a Microsoft account */ @@ -151,7 +152,7 @@ export interface AzureAccount extends azdata.Account { /** * Token returned from a request for an access token */ -interface AzureAccountSecurityToken { +export interface AzureAccountSecurityToken { /** * Access token, itself */ diff --git a/extensions/azurecore/src/account-provider/providerSettings.ts b/extensions/azurecore/src/account-provider/providerSettings.ts index ab0c326de8..cc9d0c7dae 100644 --- a/extensions/azurecore/src/account-provider/providerSettings.ts +++ b/extensions/azurecore/src/account-provider/providerSettings.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as nls from 'vscode-nls'; import { ProviderSettings } from './interfaces'; @@ -40,11 +38,11 @@ const publicAzureSettings: ProviderSettings = { } }; -/* Leaving for reference + const usGovAzureSettings: ProviderSettings = { configKey: 'enableUsGovCloud', metadata: { - displayName: localize('usGovCloudDisplayName', 'Azure (US Government)'), + displayName: localize('usGovCloudDisplayName', "Azure (US Government)"), id: 'usGovAzureCloud', settings: { host: 'https://login.microsoftonline.com/', @@ -63,32 +61,11 @@ const usGovAzureSettings: ProviderSettings = { } }; -const chinaAzureSettings: ProviderSettings = { - configKey: 'enableChinaCloud', - metadata: { - displayName: localize('chinaCloudDisplayName', 'Azure (China)'), - id: 'chinaAzureCloud', - settings: { - host: 'https://login.chinacloudapi.cn/', - clientId: 'TBD', - signInResourceId: 'https://management.core.chinacloudapi.cn/', - graphResource: { - id: 'https://graph.chinacloudapi.cn/', - endpoint: 'https://graph.chinacloudapi.cn' - }, - armResource: { - id: 'https://management.core.chinacloudapi.cn/', - endpoint: 'https://managemement.chinacloudapi.net' - }, - redirectUri: 'http://localhost/redirect' - } - } -}; const germanyAzureSettings: ProviderSettings = { configKey: 'enableGermanyCloud', metadata: { - displayName: localize('germanyCloud', 'Azure (Germany)'), + displayName: localize('germanyCloud', "Azure (Germany)"), id: 'germanyAzureCloud', settings: { host: 'https://login.microsoftazure.de/', @@ -106,7 +83,27 @@ const germanyAzureSettings: ProviderSettings = { } } }; -*/ -// TODO: Enable China, Germany, and US Gov clouds: (#3031) -export default [publicAzureSettings, /*chinaAzureSettings, germanyAzureSettings, usGovAzureSettings*/]; +const chinaAzureSettings: ProviderSettings = { + configKey: 'enableChinaCloud', + metadata: { + displayName: localize('chinaCloudDisplayName', "Azure (China)"), + id: 'chinaAzureCloud', + settings: { + host: 'https://login.chinacloudapi.cn/', + clientId: 'TBD', + signInResourceId: 'https://management.core.chinacloudapi.cn/', + graphResource: { + id: 'https://graph.chinacloudapi.cn/', + endpoint: 'https://graph.chinacloudapi.cn' + }, + armResource: { + id: 'https://management.core.chinacloudapi.cn/', + endpoint: 'https://managemement.chinacloudapi.net' + }, + redirectUri: 'http://localhost/redirect' + } + } +}; +const allSettings = [publicAzureSettings, usGovAzureSettings, germanyAzureSettings, chinaAzureSettings]; +export default allSettings; diff --git a/extensions/azurecore/src/account-provider/tokenCache.ts b/extensions/azurecore/src/account-provider/tokenCache.ts index 27eeaafe31..2e3edef727 100644 --- a/extensions/azurecore/src/account-provider/tokenCache.ts +++ b/extensions/azurecore/src/account-provider/tokenCache.ts @@ -76,20 +76,22 @@ export default class TokenCache implements adal.TokenCache { public find(query: any, callback: (error: Error, results: any[]) => void): void { let self = this; - this.doOperation(() => { - return self.readCache() - .then(cache => { - return cache.filter( - entry => TokenCache.findByPartial(entry, query) - ); - }) - .then( - results => callback(null, results), - (err) => callback(err, null) - ); + this.doOperation(async () => { + try { + const cache = await self.readCache(); + const filtered = cache.filter(entry => { + return TokenCache.findByPartial(entry, query); + }); + + callback(null, filtered); + } catch (ex) { + console.log(ex); + callback(ex, null); + } }); } + /** * Wrapper to make callback-based find method into a thenable method * @param query Partial object to use to look up tokens. Ideally should be partial of adal.TokenResponse diff --git a/extensions/azurecore/src/typings/ref.d.ts b/extensions/azurecore/src/typings/ref.d.ts index cfdf5dd135..558e12bbe2 100644 --- a/extensions/azurecore/src/typings/ref.d.ts +++ b/extensions/azurecore/src/typings/ref.d.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ /// +/// /// /// /// diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock index 1dcfe30c82..b87a360ff0 100644 --- a/extensions/azurecore/yarn.lock +++ b/extensions/azurecore/yarn.lock @@ -91,16 +91,16 @@ dependencies: "@types/node" "*" -adal-node@^0.1.28: - version "0.1.28" - resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485" - integrity sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU= +adal-node@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.2.1.tgz#19e401bd579977448c1a77ce0e5b4c9accdc334e" + integrity sha512-C/oasZuTy0NIqh5wPWjG/09XaG+zS7elC8upf1ZVExt9lSRncme4Ejbx8CKYk+wsGgj609y84txtRAXQVvqApg== dependencies: "@types/node" "^8.0.47" - async ">=0.6.0" + async "^2.6.3" date-utils "*" jws "3.x.x" - request ">= 2.52.0" + request "^2.88.0" underscore ">= 1.3.1" uuid "^3.1.0" xmldom ">= 0.1.x" @@ -204,12 +204,12 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -async@>=0.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== +async@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== dependencies: - lodash "^4.17.10" + lodash "^4.17.14" asynckit@^0.4.0: version "0.4.0" @@ -1031,7 +1031,7 @@ lead@^1.0.0: dependencies: flush-write-stream "^1.0.2" -lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.4: +lodash@^4.16.4, lodash@^4.17.14, lodash@^4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -1384,7 +1384,7 @@ replace-ext@^1.0.0: resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= -request@2.88.0, "request@>= 2.52.0", request@^2.79.0, request@^2.88.0: +request@2.88.0, request@^2.79.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== diff --git a/extensions/package.json b/extensions/package.json index d70cdbe0ca..688a5c0506 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.6.3" + "typescript": "3.7.2" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 2595e3b441..6cae6ac12d 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.6.3: - version "3.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" - integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== +typescript@3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" + integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== diff --git a/src/sql/workbench/contrib/accounts/browser/accountDialog.ts b/src/sql/workbench/contrib/accounts/browser/accountDialog.ts index af2ac65063..6f89f75a41 100644 --- a/src/sql/workbench/contrib/accounts/browser/accountDialog.ts +++ b/src/sql/workbench/contrib/accounts/browser/accountDialog.ts @@ -188,6 +188,7 @@ export class AccountDialog extends Modal { const buttonSection = DOM.append(this._noaccountViewContainer, DOM.$('div.button-section')); this._addAccountButton = new Button(buttonSection); this._addAccountButton.label = localize('accountDialog.addConnection', "Add an account"); + this._register(this._addAccountButton.onDidClick(() => { (values(this._providerViewsMap)[0]).addAccountAction.run(); }));