mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
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
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
|
"enableProposedApi": true,
|
||||||
"main": "./out/extension",
|
"main": "./out/extension",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -179,7 +180,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-resourcegraph": "^2.0.0",
|
"@azure/arm-resourcegraph": "^2.0.0",
|
||||||
"@azure/arm-subscriptions": "1.0.0",
|
"@azure/arm-subscriptions": "1.0.0",
|
||||||
"adal-node": "^0.1.28",
|
"adal-node": "^0.2.1",
|
||||||
"request": "2.88.0",
|
"request": "2.88.0",
|
||||||
"vscode-nls": "^4.0.0"
|
"vscode-nls": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,5 +18,6 @@
|
|||||||
"config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled",
|
"config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled",
|
||||||
"config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled",
|
"config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled",
|
||||||
"config.enableChinaCloudDescription": "Should Azure China integration be enabled",
|
"config.enableChinaCloudDescription": "Should Azure China integration be enabled",
|
||||||
"config.enableGermanyCloudDescription": "Should Azure Germany integration be enabled"
|
"config.enableGermanyCloudDescription": "Should Azure Germany integration be enabled",
|
||||||
|
"config.useNewSignInExperience": "Use the new Azure sign in experience"
|
||||||
}
|
}
|
||||||
@@ -432,6 +432,7 @@ export class AzureAccountProvider implements azdata.AccountProvider {
|
|||||||
displayName: displayName
|
displayName: displayName
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
|
providerSettings: this._metadata,
|
||||||
isMsAccount: msa,
|
isMsAccount: msa,
|
||||||
tenants: tenants
|
tenants: tenants
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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<azdata.Account[]> {
|
||||||
|
return this._initialize(storedAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initialize(storedAccounts: azdata.Account[]): Promise<azdata.Account[]> {
|
||||||
|
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<TokenResponse> {
|
||||||
|
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<AzureAccountSecurityTokenCollection> {
|
||||||
|
const resourceIdMap = new Map<azdata.AzureResource, string>([
|
||||||
|
[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<azdata.Account | azdata.PromptFailedResult> {
|
||||||
|
return this._prompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async _prompt(): Promise<azdata.Account | azdata.PromptFailedResult> {
|
||||||
|
if (this.isInitialized === false) {
|
||||||
|
vscode.window.showInformationMessage(notInitalizedMessage);
|
||||||
|
return { canceled: false };
|
||||||
|
}
|
||||||
|
const pathMappings = new Map<string, (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => 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<AzureAccount>((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<string, (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => 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<any> {
|
||||||
|
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<Tenant[]> {
|
||||||
|
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<void> {
|
||||||
|
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<TokenResponse> {
|
||||||
|
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<string, (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => 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<number> {
|
||||||
|
let portTimer: NodeJS.Timer;
|
||||||
|
const cancelPortTimer = (() => {
|
||||||
|
clearTimeout(portTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = new Promise<number>((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<azdata.Account | azdata.PromptFailedResult> {
|
||||||
|
return this._refresh(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _refresh(account: azdata.Account): Promise<azdata.Account | azdata.PromptFailedResult> {
|
||||||
|
return this.prompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface method
|
||||||
|
clear(accountKey: azdata.AccountKey): Thenable<void> {
|
||||||
|
return this._clear(accountKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _clear(accountKey: azdata.AccountKey): Promise<void> {
|
||||||
|
// 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<void> {
|
||||||
|
return this._autoOAuthCancelled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _autoOAuthCancelled(): Promise<void> {
|
||||||
|
// I don't think we need this?
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import * as constants from '../constants';
|
import * as constants from '../constants';
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as events from 'events';
|
import * as events from 'events';
|
||||||
@@ -13,7 +11,8 @@ import * as path from 'path';
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import CredentialServiceTokenCache from './tokenCache';
|
import CredentialServiceTokenCache from './tokenCache';
|
||||||
import providerSettings from './providerSettings';
|
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';
|
import { AzureAccountProviderMetadata, ProviderSettings } from './interfaces';
|
||||||
|
|
||||||
let localize = nls.loadMessageBundle();
|
let localize = nls.loadMessageBundle();
|
||||||
@@ -26,7 +25,7 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
|||||||
|
|
||||||
// MEMBER VARIABLES ////////////////////////////////////////////////////////
|
// MEMBER VARIABLES ////////////////////////////////////////////////////////
|
||||||
private _accountDisposals: { [accountProviderId: string]: vscode.Disposable };
|
private _accountDisposals: { [accountProviderId: string]: vscode.Disposable };
|
||||||
private _accountProviders: { [accountProviderId: string]: AzureAccountProvider };
|
private _accountProviders: { [accountProviderId: string]: azdata.AccountProvider };
|
||||||
private _credentialProvider: azdata.CredentialProvider;
|
private _credentialProvider: azdata.CredentialProvider;
|
||||||
private _configChangePromiseChain: Thenable<void>;
|
private _configChangePromiseChain: Thenable<void>;
|
||||||
private _currentConfig: vscode.WorkspaceConfiguration;
|
private _currentConfig: vscode.WorkspaceConfiguration;
|
||||||
@@ -69,10 +68,11 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
|||||||
|
|
||||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||||
private onClearTokenCache(): Thenable<void> {
|
private onClearTokenCache(): Thenable<void> {
|
||||||
let self = this;
|
// let self = this;
|
||||||
|
|
||||||
let promises: Thenable<void>[] = providerSettings.map(provider => {
|
let promises: Thenable<void>[] = providerSettings.map(provider => {
|
||||||
return self._accountProviders[provider.metadata.id].clearTokenCache();
|
// return self._accountProviders[provider.metadata.id].clearTokenCache();
|
||||||
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
@@ -129,14 +129,23 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerAccountProvider(provider: ProviderSettings): Thenable<void> {
|
private registerAccountProvider(provider: ProviderSettings): Thenable<void> {
|
||||||
|
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
//let config = vscode.workspace.getConfiguration(AzureAccountProviderService.ConfigurationSection);
|
||||||
|
|
||||||
let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
|
let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
|
||||||
let tokenCachePath = path.join(this._userStoragePath, tokenCacheKey);
|
let tokenCachePath = path.join(this._userStoragePath, tokenCacheKey);
|
||||||
let tokenCache = new CredentialServiceTokenCache(self._credentialProvider, tokenCacheKey, tokenCachePath);
|
let tokenCache = new CredentialServiceTokenCache(self._credentialProvider, tokenCacheKey, tokenCachePath);
|
||||||
let accountProvider = new AzureAccountProvider(<AzureAccountProviderMetadata>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._accountProviders[provider.metadata.id] = accountProvider;
|
||||||
self._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider);
|
self._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider);
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export interface AzureAccountProviderMetadata extends azdata.AccountProviderMeta
|
|||||||
* Properties specific to an Azure account
|
* Properties specific to an Azure account
|
||||||
*/
|
*/
|
||||||
interface AzureAccountProperties {
|
interface AzureAccountProperties {
|
||||||
|
providerSettings: AzureAccountProviderMetadata;
|
||||||
/**
|
/**
|
||||||
* Whether or not the account is a Microsoft account
|
* 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
|
* Token returned from a request for an access token
|
||||||
*/
|
*/
|
||||||
interface AzureAccountSecurityToken {
|
export interface AzureAccountSecurityToken {
|
||||||
/**
|
/**
|
||||||
* Access token, itself
|
* Access token, itself
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import { ProviderSettings } from './interfaces';
|
import { ProviderSettings } from './interfaces';
|
||||||
|
|
||||||
@@ -40,11 +38,11 @@ const publicAzureSettings: ProviderSettings = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Leaving for reference
|
|
||||||
const usGovAzureSettings: ProviderSettings = {
|
const usGovAzureSettings: ProviderSettings = {
|
||||||
configKey: 'enableUsGovCloud',
|
configKey: 'enableUsGovCloud',
|
||||||
metadata: {
|
metadata: {
|
||||||
displayName: localize('usGovCloudDisplayName', 'Azure (US Government)'),
|
displayName: localize('usGovCloudDisplayName', "Azure (US Government)"),
|
||||||
id: 'usGovAzureCloud',
|
id: 'usGovAzureCloud',
|
||||||
settings: {
|
settings: {
|
||||||
host: 'https://login.microsoftonline.com/',
|
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 = {
|
const germanyAzureSettings: ProviderSettings = {
|
||||||
configKey: 'enableGermanyCloud',
|
configKey: 'enableGermanyCloud',
|
||||||
metadata: {
|
metadata: {
|
||||||
displayName: localize('germanyCloud', 'Azure (Germany)'),
|
displayName: localize('germanyCloud', "Azure (Germany)"),
|
||||||
id: 'germanyAzureCloud',
|
id: 'germanyAzureCloud',
|
||||||
settings: {
|
settings: {
|
||||||
host: 'https://login.microsoftazure.de/',
|
host: 'https://login.microsoftazure.de/',
|
||||||
@@ -106,7 +83,27 @@ const germanyAzureSettings: ProviderSettings = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: Enable China, Germany, and US Gov clouds: (#3031)
|
const chinaAzureSettings: ProviderSettings = {
|
||||||
export default <ProviderSettings[]>[publicAzureSettings, /*chinaAzureSettings, germanyAzureSettings, usGovAzureSettings*/];
|
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;
|
||||||
|
|||||||
@@ -76,20 +76,22 @@ export default class TokenCache implements adal.TokenCache {
|
|||||||
public find(query: any, callback: (error: Error, results: any[]) => void): void {
|
public find(query: any, callback: (error: Error, results: any[]) => void): void {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
this.doOperation(() => {
|
this.doOperation(async () => {
|
||||||
return self.readCache()
|
try {
|
||||||
.then(cache => {
|
const cache = await self.readCache();
|
||||||
return cache.filter(
|
const filtered = cache.filter(entry => {
|
||||||
entry => TokenCache.findByPartial(entry, query)
|
return TokenCache.findByPartial(entry, query);
|
||||||
);
|
});
|
||||||
})
|
|
||||||
.then(
|
callback(null, filtered);
|
||||||
results => callback(null, results),
|
} catch (ex) {
|
||||||
(err) => callback(err, null)
|
console.log(ex);
|
||||||
);
|
callback(ex, null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper to make callback-based find method into a thenable method
|
* 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
|
* @param query Partial object to use to look up tokens. Ideally should be partial of adal.TokenResponse
|
||||||
|
|||||||
1
extensions/azurecore/src/typings/ref.d.ts
vendored
1
extensions/azurecore/src/typings/ref.d.ts
vendored
@@ -4,6 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||||
|
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||||
/// <reference types='@types/node'/>
|
/// <reference types='@types/node'/>
|
||||||
|
|||||||
@@ -91,16 +91,16 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
adal-node@^0.1.28:
|
adal-node@^0.2.1:
|
||||||
version "0.1.28"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485"
|
resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.2.1.tgz#19e401bd579977448c1a77ce0e5b4c9accdc334e"
|
||||||
integrity sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=
|
integrity sha512-C/oasZuTy0NIqh5wPWjG/09XaG+zS7elC8upf1ZVExt9lSRncme4Ejbx8CKYk+wsGgj609y84txtRAXQVvqApg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "^8.0.47"
|
"@types/node" "^8.0.47"
|
||||||
async ">=0.6.0"
|
async "^2.6.3"
|
||||||
date-utils "*"
|
date-utils "*"
|
||||||
jws "3.x.x"
|
jws "3.x.x"
|
||||||
request ">= 2.52.0"
|
request "^2.88.0"
|
||||||
underscore ">= 1.3.1"
|
underscore ">= 1.3.1"
|
||||||
uuid "^3.1.0"
|
uuid "^3.1.0"
|
||||||
xmldom ">= 0.1.x"
|
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"
|
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||||
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
||||||
|
|
||||||
async@>=0.6.0:
|
async@^2.6.3:
|
||||||
version "2.6.1"
|
version "2.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
|
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||||
integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
|
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.10"
|
lodash "^4.17.14"
|
||||||
|
|
||||||
asynckit@^0.4.0:
|
asynckit@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
@@ -1031,7 +1031,7 @@ lead@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flush-write-stream "^1.0.2"
|
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"
|
version "4.17.15"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
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"
|
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
|
||||||
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
|
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"
|
version "2.88.0"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
||||||
integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
|
integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Dependencies shared by all extensions",
|
"description": "Dependencies shared by all extensions",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"typescript": "3.6.3"
|
"typescript": "3.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node ./postinstall"
|
"postinstall": "node ./postinstall"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
typescript@3.6.3:
|
typescript@3.7.2:
|
||||||
version "3.6.3"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
|
||||||
integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==
|
integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export class AccountDialog extends Modal {
|
|||||||
const buttonSection = DOM.append(this._noaccountViewContainer, DOM.$('div.button-section'));
|
const buttonSection = DOM.append(this._noaccountViewContainer, DOM.$('div.button-section'));
|
||||||
this._addAccountButton = new Button(buttonSection);
|
this._addAccountButton = new Button(buttonSection);
|
||||||
this._addAccountButton.label = localize('accountDialog.addConnection', "Add an account");
|
this._addAccountButton.label = localize('accountDialog.addConnection', "Add an account");
|
||||||
|
|
||||||
this._register(this._addAccountButton.onDidClick(() => {
|
this._register(this._addAccountButton.onDidClick(() => {
|
||||||
(<IProviderViewUiComponent>values(this._providerViewsMap)[0]).addAccountAction.run();
|
(<IProviderViewUiComponent>values(this._providerViewsMap)[0]).addAccountAction.run();
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user