diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index a0b3eb0f5e..2a2507607c 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -12,6 +12,7 @@ expressly granted herein, whether by implication, estoppel or otherwise. angular2-grid: https://github.com/BTMorton/angular2-grid angular2-slickgrid: https://github.com/Microsoft/angular2-slickgrid applicationinsights: https://github.com/Microsoft/ApplicationInsights-node.js + axios: https://github.com/axios/axios bootstrap: https://github.com/twbs/bootstrap chart.js: https://github.com/Timer/chartjs chokidar: https://github.com/paulmillr/chokidar @@ -38,6 +39,7 @@ expressly granted herein, whether by implication, estoppel or otherwise. jschardet: https://github.com/aadsm/jschardet jupyter-powershell: https://github.com/vors/jupyter-powershell JupyterLab: https://github.com/jupyterlab/jupyterlab + keytar: https://github.com/atom/node-keytar make-error: https://github.com/JsCommunity/make-error minimist: https://github.com/substack/minimist moment: https://github.com/moment/moment @@ -50,6 +52,7 @@ expressly granted herein, whether by implication, estoppel or otherwise. primeng: https://github.com/primefaces/primeng process-nextick-args: https://github.com/calvinmetcalf/process-nextick-args pty.js: https://github.com/chjj/pty.js + qs: https://github.com/ljharb/qs reflect-metadata: https://github.com/rbuckton/reflect-metadata request: https://github.com/request/request rxjs: https://github.com/ReactiveX/RxJS diff --git a/extensions/azurecore/extension.webpack.config.js b/extensions/azurecore/extension.webpack.config.js index 35b95ccffc..efc18ebf4a 100644 --- a/extensions/azurecore/extension.webpack.config.js +++ b/extensions/azurecore/extension.webpack.config.js @@ -13,5 +13,8 @@ module.exports = withDefaults({ context: __dirname, entry: { extension: './src/extension.ts' + }, + externals: { + 'keytar': 'commonjs keytar' } }); diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index 40515642a8..42884380be 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -33,12 +33,43 @@ }, { "type": "object", - "title": "Azure Account Configuration", + "title": "%config.azureAccountConfigurationSection%", "properties": { - "accounts.azure.enablePublicCloud": { + "accounts.azure.cloud.enablePublicCloud": { "type": "boolean", "default": true, "description": "%config.enablePublicCloudDescription%" + }, + "accounts.azure.cloud.enableUsGovCloud": { + "type": "boolean", + "default": false, + "description": "%config.enableUsGovCloudDescription%" + }, + "accounts.azure.cloud.enableGermanyCloud": { + "type": "boolean", + "default": false, + "description": "%config.enableGermanyCloudDescription%" + }, + "accounts.azure.cloud.enableChinaCloud": { + "type": "boolean", + "default": false, + "description": "%config.enableChinaCloudDescription%" + } + } + }, + { + "type": "object", + "title": "%config.azureAuthMethodConfigurationSection%", + "properties": { + "accounts.azure.auth.codeGrant": { + "type": "boolean", + "default": true, + "description": "%config.azureCodeGrantMethod%" + }, + "accounts.azure.auth.deviceCode": { + "type": "boolean", + "default": false, + "description": "%config.azureDeviceCodeMethod%" } } }, @@ -199,12 +230,16 @@ "@azure/arm-subscriptions": "1.0.0", "adal-node": "^0.2.1", "axios": "^0.19.2", + "keytar": "^5.4.0", + "qs": "^6.9.1", "request": "2.88.0", "vscode-nls": "^4.0.0" }, "devDependencies": { + "@types/keytar": "^4.4.2", "@types/mocha": "^5.2.5", "@types/node": "^12.11.7", + "@types/qs": "^6.9.1", "@types/request": "^2.48.1", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json index c7083e86fc..91e99999ea 100644 --- a/extensions/azurecore/package.nls.json +++ b/extensions/azurecore/package.nls.json @@ -17,9 +17,13 @@ "azure.accounts.getSubscriptions.title": "Get Azure Account Subscriptions", "azure.accounts.getResourceGroups.title": "Get Azure Account Subscription Resource Groups", + "config.azureAccountConfigurationSection": "Azure Account Configuration", "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", + "config.azureAuthMethodConfigurationSection": "Azure Authentication Method", + "config.azureCodeGrantMethod": "Code Grant Method", + "config.azureDeviceCodeMethod": "Device Code Method", "config.enableArcFeatures": "Should features related to Azure Arc be enabled (preview)" } diff --git a/extensions/azurecore/src/account-provider/auths/azureAuth.ts b/extensions/azurecore/src/account-provider/auths/azureAuth.ts new file mode 100644 index 0000000000..73ef29beb7 --- /dev/null +++ b/extensions/azurecore/src/account-provider/auths/azureAuth.ts @@ -0,0 +1,504 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls from 'vscode-nls'; +import axios, { AxiosResponse } from 'axios'; +import * as qs from 'qs'; +import * as url from 'url'; + +import { + AzureAccountProviderMetadata, + Tenant, + AzureAccount, + Resource, + AzureAuthType, + Subscription +} from '../interfaces'; + +import { SimpleTokenCache } from '../simpleTokenCache'; +import { MemoryDatabase } from '../utils/memoryDatabase'; +const localize = nls.loadMessageBundle(); + +export interface AccountKey { + /** + * Account Key - uniquely identifies an account + */ + key: string +} + +export interface AccessToken extends AccountKey { + /** + * Access Token + */ + token: string; +} + +export interface RefreshToken extends AccountKey { + /** + * Refresh Token + */ + token: string; + + /** + * Account Key + */ + key: string +} + +export interface TokenResponse { + [tenantId: string]: Token +} + +export interface Token extends AccountKey { + /** + * Access token + */ + token: string; + + /** + * TokenType + */ + tokenType: string; +} + +export interface TokenClaims { // https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens + aud: string; + iss: string; + iat: number; + idp: string, + nbf: number; + exp: number; + c_hash: string; + at_hash: string; + aio: string; + preferred_username: string; + email: string; + name: string; + nonce: string; + oid: string; + roles: string[]; + rh: string; + sub: string; + tid: string; + unique_name: string; + uti: string; + ver: string; +} + +export type TokenRefreshResponse = { accessToken: AccessToken, refreshToken: RefreshToken, tokenClaims: TokenClaims }; + +export abstract class AzureAuth { + protected readonly memdb = new MemoryDatabase(); + + protected readonly WorkSchoolAccountType: string = 'work_school'; + protected readonly MicrosoftAccountType: string = 'microsoft'; + + protected readonly loginEndpointUrl: string; + protected readonly commonTenant: string; + protected readonly redirectUri: string; + protected readonly scopes: string[]; + protected readonly scopesString: string; + protected readonly clientId: string; + protected readonly resources: Resource[]; + + + constructor( + protected readonly metadata: AzureAccountProviderMetadata, + protected readonly tokenCache: SimpleTokenCache, + protected readonly context: vscode.ExtensionContext, + protected readonly authType: AzureAuthType, + public readonly userFriendlyName: string + ) { + this.loginEndpointUrl = this.metadata.settings.host; + this.commonTenant = 'common'; + this.redirectUri = this.metadata.settings.redirectUri; + this.clientId = this.metadata.settings.clientId; + + this.resources = [ + this.metadata.settings.armResource, this.metadata.settings.sqlResource, + this.metadata.settings.graphResource, this.metadata.settings.ossRdbmsResource + ]; + + this.scopes = [...this.metadata.settings.scopes]; + this.scopesString = this.scopes.join(' '); + } + + public abstract async login(): Promise; + + public abstract async autoOAuthCancelled(): Promise; + + + public async refreshAccess(account: azdata.Account): Promise { + const response = await this.getCachedToken(account.key); + if (!response) { + account.isStale = true; + return account; + } + + const refreshToken = response.refreshToken; + if (!refreshToken || !refreshToken.key) { + account.isStale = true; + return account; + } + + try { + await this.refreshAccessToken(account.key, refreshToken); + } catch (ex) { + if (ex.message) { + await vscode.window.showErrorMessage(ex.message); + } + console.log(ex); + } + return account; + } + + + public async getSecurityToken(account: azdata.Account, azureResource: azdata.AzureResource): Promise { + const resource = this.resources.find(s => s.azureResourceId === azureResource); + if (!resource) { + return undefined; + } + const azureAccount = account as AzureAccount; + const response: TokenResponse = {}; + + for (const tenant of azureAccount.properties.tenants) { + let cachedTokens = await this.getCachedToken(account.key, resource.id, tenant.id); + // Check expiration + if (cachedTokens) { + const expiresOn = Number(this.memdb.get(this.createMemdbString(account.key.accountId, tenant.id, resource.id))); + const currentTime = new Date().getTime() / 1000; + + if (!Number.isNaN(expiresOn)) { + const remainingTime = expiresOn - currentTime; + const fiveMinutes = 5 * 60; + // If the remaining time is less than five minutes, assume the token has expired. It's too close to expiration to be meaningful. + if (remainingTime < fiveMinutes) { + cachedTokens = undefined; + } + } else { + // No expiration date, assume expired. + cachedTokens = undefined; + console.info('Assuming expired token due to no expiration date - this is expected on first launch.'); + } + + } + + // Refresh + if (!cachedTokens) { + + const baseToken = await this.getCachedToken(account.key); + if (!baseToken) { + return undefined; + } + + await this.refreshAccessToken(account.key, baseToken.refreshToken, tenant, resource); + cachedTokens = await this.getCachedToken(account.key, resource.id, tenant.id); + if (!cachedTokens) { + return undefined; + } + } + const { accessToken } = cachedTokens; + response[tenant.id] = { + token: accessToken.token, + key: accessToken.key, + tokenType: 'Bearer' + }; + } + + if (azureAccount.properties.subscriptions) { + azureAccount.properties.subscriptions.forEach(subscription => { + response[subscription.id] = { + ...response[subscription.tenantId] + }; + }); + } + + return response; + } + + public async clearCredentials(account: azdata.AccountKey): Promise { + try { + return this.deleteAccountCache(account); + } catch (ex) { + const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache."); + vscode.window.showErrorMessage(msg); + console.error('Error when removing tokens.', ex); + } + } + + protected toBase64UrlEncoding(base64string: string): string { + return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding + } + + protected async makePostRequest(uri: string, postData: { [key: string]: string }) { + const config = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + return axios.post(uri, qs.stringify(postData), config); + } + + protected async makeGetRequest(token: string, uri: string): Promise> { + const config = { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }; + + return axios.get(uri, config); + } + + protected async getTenants(token: AccessToken): Promise { + interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list + id: string + tenantId: string + displayName?: string + tenantCategory?: string + } + + const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01'); + try { + const tenantResponse = await this.makeGetRequest(token.token, tenantUri); + const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => { + return { + id: tenantInfo.tenantId, + displayName: tenantInfo.displayName ? tenantInfo.displayName : localize('azureWorkAccountDisplayName', "Work or school account"), + userId: token.key, + tenantCategory: tenantInfo.tenantCategory + } as Tenant; + }); + + const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === 'Home'); + if (homeTenantIndex >= 0) { + const homeTenant = tenants.splice(homeTenantIndex, 1); + tenants.unshift(homeTenant[0]); + } + + return tenants; + } catch (ex) { + console.log(ex); + throw new Error('Error retreiving tenant information'); + } + } + + protected async getSubscriptions(account: AzureAccount): Promise { + interface SubscriptionResponse { // https://docs.microsoft.com/en-us/rest/api/resources/subscriptions/list + subscriptionId: string + tenantId: string + displayName: string + } + const allSubs: Subscription[] = []; + const tokens = await this.getSecurityToken(account, azdata.AzureResource.ResourceManagement); + + for (const tenant of account.properties.tenants) { + const token = tokens[tenant.id]; + const subscriptionUri = url.resolve(this.metadata.settings.armResource.endpoint, 'subscriptions?api-version=2019-11-01'); + try { + const subscriptionResponse = await this.makeGetRequest(token.token, subscriptionUri); + const subscriptions: Subscription[] = subscriptionResponse.data.value.map((subscriptionInfo: SubscriptionResponse) => { + return { + id: subscriptionInfo.subscriptionId, + displayName: subscriptionInfo.displayName, + tenantId: subscriptionInfo.tenantId + } as Subscription; + }); + allSubs.push(...subscriptions); + } catch (ex) { + console.log(ex); + throw new Error('Error retreiving subscription information'); + } + } + return allSubs; + } + + protected async getToken(postData: { [key: string]: string }, tenant = this.commonTenant, resourceId: string = ''): Promise { + try { + const tokenUrl = `${this.loginEndpointUrl}${tenant}/oauth2/token`; + + const tokenResponse = await this.makePostRequest(tokenUrl, postData); + const tokenClaims = this.getTokenClaims(tokenResponse.data.access_token); + + const accessToken: AccessToken = { + token: tokenResponse.data.access_token, + key: tokenClaims.email || tokenClaims.unique_name || tokenClaims.name, + }; + + this.memdb.set(this.createMemdbString(accessToken.key, tenant, resourceId), tokenResponse.data.expires_on); + + const refreshToken: RefreshToken = { + token: tokenResponse.data.refresh_token, + key: accessToken.key + }; + + return { accessToken, refreshToken, tokenClaims }; + + } catch (err) { + console.dir(err); + const msg = localize('azure.noToken', "Retrieving the token failed."); + vscode.window.showErrorMessage(msg); + throw new Error(err); + } + } + + protected getTokenClaims(accessToken: string): TokenClaims | undefined { + try { + const split = accessToken.split('.'); + return JSON.parse(Buffer.from(split[1], 'base64').toString('binary')); + } catch (ex) { + throw new Error('Unable to read token claims: ' + JSON.stringify(ex)); + } + } + + private async refreshAccessToken(account: azdata.AccountKey, rt: RefreshToken, tenant?: Tenant, resource?: Resource): Promise { + const postData: { [key: string]: string } = { + grant_type: 'refresh_token', + refresh_token: rt.token, + client_id: this.clientId, + tenant: this.commonTenant, + }; + + if (resource) { + postData.resource = resource.endpoint; + } + + const { accessToken, refreshToken } = await this.getToken(postData, tenant?.id, resource?.id); + + if (!accessToken || !refreshToken) { + console.log('Access or refresh token were undefined'); + const msg = localize('azure.refreshTokenError', "Error when refreshing your account."); + throw new Error(msg); + } + + return this.setCachedToken(account, accessToken, refreshToken, resource?.id, tenant?.id); + } + + + public async setCachedToken(account: azdata.AccountKey, accessToken: AccessToken, refreshToken: RefreshToken, resourceId?: string, tenantId?: string): Promise { + const msg = localize('azure.cacheErrorAdd', "Error when adding your account to the cache."); + resourceId = resourceId ?? ''; + tenantId = tenantId ?? ''; + if (!accessToken || !accessToken.token || !refreshToken.token || !accessToken.key) { + throw new Error(msg); + } + + try { + await this.tokenCache.saveCredential(`${account.accountId}_access_${resourceId}_${tenantId}`, JSON.stringify(accessToken)); + await this.tokenCache.saveCredential(`${account.accountId}_refresh_${resourceId}_${tenantId}`, JSON.stringify(refreshToken)); + } catch (ex) { + console.error('Error when storing tokens.', ex); + throw new Error(msg); + } + } + + public async getCachedToken(account: azdata.AccountKey, resourceId?: string, tenantId?: string): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken } | undefined> { + resourceId = resourceId ?? ''; + tenantId = tenantId ?? ''; + + let accessToken: AccessToken; + let refreshToken: RefreshToken; + try { + accessToken = JSON.parse(await this.tokenCache.getCredential(`${account.accountId}_access_${resourceId}_${tenantId}`)); + refreshToken = JSON.parse(await this.tokenCache.getCredential(`${account.accountId}_refresh_${resourceId}_${tenantId}`)); + } catch (ex) { + return undefined; + } + + if (!accessToken || !refreshToken) { + return undefined; + } + + if (!refreshToken.token || !refreshToken.key) { + return undefined; + } + + if (!accessToken.token || !accessToken.key) { + return undefined; + } + + return { + accessToken, + refreshToken + }; + + } + + public createMemdbString(accountKey: string, tenantId: string, resourceId: string): string { + return `${accountKey}_${tenantId}_${resourceId}`; + } + + public async deleteAccountCache(account: azdata.AccountKey): Promise { + const results = await this.tokenCache.findCredentials(account.accountId); + + for (let { account } of results) { + await this.tokenCache.clearCredential(account); + } + } + + public async deleteAllCache(): Promise { + const results = await this.tokenCache.findCredentials(''); + + for (let { account } of results) { + await this.tokenCache.clearCredential(account); + } + } + + public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount { + // Determine if this is a microsoft account + let accountIssuer = 'unknown'; + + if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/') { + accountIssuer = 'corp'; + } + if (tokenClaims?.idp === 'live.com') { + accountIssuer = 'msft'; + } + + const displayName = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name; + + let contextualDisplayName: string; + switch (accountIssuer) { + case 'corp': + contextualDisplayName = localize('azure.microsoftCorpAccount', "Microsoft Corp"); + break; + case 'msft': + contextualDisplayName = localize('azure.microsoftAccountDisplayName', 'Microsoft Account'); + break; + default: + contextualDisplayName = displayName; + } + + let accountType = accountIssuer === 'msft' + ? this.MicrosoftAccountType + : this.WorkSchoolAccountType; + + const account = { + key: { + providerId: this.metadata.id, + accountId: key + }, + name: key, + displayInfo: { + accountType: accountType, + userId: key, + contextualDisplayName: contextualDisplayName, + displayName + }, + properties: { + providerSettings: this.metadata, + isMsAccount: accountIssuer === 'msft', + tenants, + azureAuthType: this.authType + }, + isStale: false + } as AzureAccount; + + return account; + } +} diff --git a/extensions/azurecore/src/account-provider/auths/azureAuthCodeGrant.ts b/extensions/azurecore/src/account-provider/auths/azureAuthCodeGrant.ts new file mode 100644 index 0000000000..032e9264b0 --- /dev/null +++ b/extensions/azurecore/src/account-provider/auths/azureAuthCodeGrant.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * 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 crypto from 'crypto'; +import * as nls from 'vscode-nls'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as http from 'http'; +import * as qs from 'qs'; + +import { + AzureAuth, + AccessToken, + RefreshToken, + TokenClaims, + TokenRefreshResponse, +} from './azureAuth'; + +import { + AzureAccountProviderMetadata, + AzureAuthType, + Deferred +} from '../interfaces'; + +import { SimpleWebServer } from '../utils/simpleWebServer'; +import { SimpleTokenCache } from '../simpleTokenCache'; +const localize = nls.loadMessageBundle(); + +export class AzureAuthCodeGrant extends AzureAuth { + private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', "Azure Auth Code Grant"); + private server: SimpleWebServer; + + constructor(metadata: AzureAccountProviderMetadata, + tokenCache: SimpleTokenCache, + context: vscode.ExtensionContext) { + super(metadata, tokenCache, context, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME); + } + + public async autoOAuthCancelled(): Promise { + return this.server.shutdown(); + } + + public async login(): Promise { + let authCompleteDeferred: Deferred; + let authCompletePromise = new Promise((resolve, reject) => authCompleteDeferred = { resolve, reject }); + + this.server = new SimpleWebServer(); + const nonce = crypto.randomBytes(16).toString('base64'); + let serverPort: string; + + try { + serverPort = await this.server.startup(); + } catch (err) { + const msg = localize('azure.serverCouldNotStart', 'Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings.'); + await vscode.window.showErrorMessage(msg); + console.dir(err); + return { canceled: false } as azdata.PromptFailedResult; + } + + + // The login code to use + let loginUrl: string; + let codeVerifier: string; + { + codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); + const state = `${serverPort},${encodeURIComponent(nonce)}`; + const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); + const loginQuery = { + response_type: 'code', + response_mode: 'query', + client_id: this.clientId, + redirect_uri: this.redirectUri, + state, + prompt: 'select_account', + code_challenge_method: 'S256', + code_challenge: codeChallenge, + resource: this.metadata.settings.signInResourceId + }; + loginUrl = `${this.loginEndpointUrl}${this.commonTenant}/oauth2/authorize?${qs.stringify(loginQuery)}`; + } + + await vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${serverPort}/signin?nonce=${encodeURIComponent(nonce)}`)); + + const authenticatedCode = await this.addServerListeners(this.server, nonce, loginUrl, authCompletePromise); + + let tokenClaims: TokenClaims; + let accessToken: AccessToken; + let refreshToken: RefreshToken; + + try { + const { accessToken: at, refreshToken: rt, tokenClaims: tc } = await this.getTokenWithAuthCode(authenticatedCode, codeVerifier, this.redirectUri); + tokenClaims = tc; + accessToken = at; + refreshToken = rt; + } catch (ex) { + if (ex.msg) { + await vscode.window.showErrorMessage(ex.msg); + } + console.log(ex); + } + + if (!accessToken) { + const msg = localize('azure.tokenFail', "Failure when retreiving tokens."); + authCompleteDeferred.reject(new Error(msg)); + throw Error('Failure when retreiving tokens'); + } + + const tenants = await this.getTenants(accessToken); + + try { + await this.setCachedToken({ accountId: accessToken.key, providerId: this.metadata.id }, accessToken, refreshToken); + } catch (ex) { + console.log(ex); + if (ex.msg) { + await vscode.window.showErrorMessage(ex.msg); + authCompleteDeferred.reject(ex); + } else { + authCompleteDeferred.reject(new Error('There was an issue when storing the cache.')); + } + + return { canceled: false } as azdata.PromptFailedResult; + } + + const account = this.createAccount(tokenClaims, accessToken.key, tenants); + + const subscriptions = await this.getSubscriptions(account); + account.properties.subscriptions = subscriptions; + + authCompleteDeferred.resolve(); + return account; + } + + private async addServerListeners(server: SimpleWebServer, nonce: string, loginUrl: string, authComplete: Promise): Promise { + const mediaPath = path.join(this.context.extensionPath, 'media'); + + // Utility function + const sendFile = async (res: http.ServerResponse, filePath: string, contentType: string): Promise => { + let fileContents; + try { + fileContents = await fs.readFile(filePath); + } catch (ex) { + console.error(ex); + res.writeHead(400); + res.end(); + return; + } + + res.writeHead(200, { + 'Content-Length': fileContents.length, + 'Content-Type': contentType + }); + + res.end(fileContents); + }; + + server.on('/landing.css', (req, reqUrl, res) => { + sendFile(res, path.join(mediaPath, 'landing.css'), 'text/css; charset=utf-8').catch(console.error); + }); + + server.on('/SignIn.svg', (req, reqUrl, res) => { + sendFile(res, path.join(mediaPath, 'SignIn.svg'), 'image/svg+xml').catch(console.error); + }); + + server.on('/signin', (req, reqUrl, res) => { + let receivedNonce: string = reqUrl.query.nonce as string; + receivedNonce = receivedNonce.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 Azure Data Studio and try again.")); + res.end(); + console.error('nonce no match', receivedNonce, nonce); + return; + } + res.writeHead(302, { Location: loginUrl }); + res.end(); + }); + + return new Promise((resolve, reject) => { + server.on('/callback', (req, reqUrl, res) => { + 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(); + reject(new Error('State mismatch')); + return; + } + + if (stateSplit[1] !== encodeURIComponent(nonce)) { + res.writeHead(400, { 'content-type': 'text/html' }); + res.write(localize('azureAuth.nonceError', "Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again.")); + res.end(); + reject(new Error('Nonce mismatch')); + return; + } + + resolve(code); + + authComplete.then(() => { + sendFile(res, path.join(mediaPath, 'landing.html'), 'text/html; charset=utf-8').catch(console.error); + }, (ex: Error) => { + res.writeHead(400, { 'content-type': 'text/html' }); + res.write(ex.message); + res.end(); + }); + }); + }); + } + + private async getTokenWithAuthCode(authCode: string, codeVerifier: string, redirectUri: string): Promise { + const postData = { + grant_type: 'authorization_code', + code: authCode, + client_id: this.clientId, + code_verifier: codeVerifier, + redirect_uri: redirectUri, + resource: this.metadata.settings.signInResourceId + }; + + return this.getToken(postData); + } +} diff --git a/extensions/azurecore/src/account-provider/auths/azureDeviceCode.ts b/extensions/azurecore/src/account-provider/auths/azureDeviceCode.ts new file mode 100644 index 0000000000..f563db446b --- /dev/null +++ b/extensions/azurecore/src/account-provider/auths/azureDeviceCode.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls from 'vscode-nls'; +import { + AzureAuth, + TokenClaims, + AccessToken, + RefreshToken + +} from './azureAuth'; + +import { + AzureAccountProviderMetadata, + AzureAccount, + AzureAuthType, + // Tenant, + // Subscription +} from '../interfaces'; + +import { SimpleTokenCache } from '../simpleTokenCache'; +const localize = nls.loadMessageBundle(); + +interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code + device_code: string, + expires_in: number; + interval: number; + message: string; + user_code: string; + verification_url: string +} + +interface DeviceCodeLoginResult { + token_type: string, + scope: string, + expires_in: number, + access_token: string, + refresh_token: string, +} + +export class AzureDeviceCode extends AzureAuth { + + private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureDeviceCodeAuth', "Azure Device Code"); + private readonly pageTitle: string; + constructor(metadata: AzureAccountProviderMetadata, + tokenCache: SimpleTokenCache, + context: vscode.ExtensionContext) { + super(metadata, tokenCache, context, AzureAuthType.AuthCodeGrant, AzureDeviceCode.USER_FRIENDLY_NAME); + this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName); + + } + + public async login(): Promise { + try { + const uri = `${this.loginEndpointUrl}/${this.commonTenant}/oauth2/devicecode`; + const postResult = await this.makePostRequest(uri, { + client_id: this.clientId, + resource: this.metadata.settings.signInResourceId + }); + + const initialDeviceLogin: DeviceCodeLogin = postResult.data; + + await azdata.accounts.beginAutoOAuthDeviceCode(this.metadata.id, this.pageTitle, initialDeviceLogin.message, initialDeviceLogin.user_code, initialDeviceLogin.verification_url); + + const finalDeviceLogin = await this.setupPolling(initialDeviceLogin); + + let tokenClaims: TokenClaims; + let accessToken: AccessToken; + let refreshToken: RefreshToken; + // let tenants: Tenant[]; + // let subscriptions: Subscription[]; + + tokenClaims = this.getTokenClaims(finalDeviceLogin.access_token); + + accessToken = { + token: finalDeviceLogin.access_token, + key: tokenClaims.email || tokenClaims.unique_name || tokenClaims.name, + }; + + refreshToken = { + token: finalDeviceLogin.refresh_token, + key: accessToken.key, + }; + + await this.setCachedToken({ accountId: accessToken.key, providerId: this.metadata.id }, accessToken, refreshToken); + + const tenants = await this.getTenants(accessToken); + const account = this.createAccount(tokenClaims, accessToken.key, tenants); + const subscriptions = await this.getSubscriptions(account); + account.properties.subscriptions = subscriptions; + return account; + } catch (ex) { + console.log(ex); + if (ex.msg) { + vscode.window.showErrorMessage(ex.msg); + } + return { canceled: false }; + } finally { + azdata.accounts.endAutoOAuthDeviceCode(); + } + } + + + private setupPolling(info: DeviceCodeLogin): Promise { + const timeoutMessage = localize('azure.timeoutDeviceCode', 'Timed out when waiting for device code login.'); + const fiveMinutes = 5 * 60 * 1000; + + return new Promise((resolve, reject) => { + let timeout: NodeJS.Timer; + + const timer = setInterval(async () => { + const x = await this.checkForResult(info); + if (!x.access_token) { + return; + } + clearTimeout(timeout); + clearInterval(timer); + resolve(x); + }, info.interval * 1000); + + timeout = setTimeout(() => { + clearInterval(timer); + reject(new Error(timeoutMessage)); + }, fiveMinutes); + }); + } + + private async checkForResult(info: DeviceCodeLogin): Promise { + const msg = localize('azure.deviceCodeCheckFail', "Error encountered when trying to check for login results"); + try { + const uri = `${this.loginEndpointUrl}/${this.commonTenant}/oauth2/token`; + const postData = { + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + client_id: this.clientId, + tenant: this.commonTenant, + code: info.device_code + }; + + const postResult = await this.makePostRequest(uri, postData); + + const result: DeviceCodeLoginResult = postResult.data; + + return result; + } catch (ex) { + console.log(ex); + throw new Error(msg); + } + } + + + public async autoOAuthCancelled(): Promise { + return azdata.accounts.endAutoOAuthDeviceCode(); + } + +} diff --git a/extensions/azurecore/src/account-provider/azureAccountProvider.ts b/extensions/azurecore/src/account-provider/azureAccountProvider.ts index 8f145d42f7..5eaa9f0249 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProvider.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProvider.ts @@ -3,446 +3,163 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as adal from 'adal-node'; import * as azdata from 'azdata'; -import * as request from 'request'; -import * as nls from 'vscode-nls'; import * as vscode from 'vscode'; -import * as url from 'url'; +import * as nls from 'vscode-nls'; + import { - AzureAccount, AzureAccountProviderMetadata, - AzureAccountSecurityTokenCollection, - Tenant + AzureAuthType, + Deferred } from './interfaces'; -import TokenCache from './tokenCache'; + +import { SimpleTokenCache } from './simpleTokenCache'; +import { AzureAuth, TokenResponse } from './auths/azureAuth'; +import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant'; +import { AzureDeviceCode } from './auths/azureDeviceCode'; const localize = nls.loadMessageBundle(); export class AzureAccountProvider implements azdata.AccountProvider { - // CONSTANTS /////////////////////////////////////////////////////////// - private static WorkSchoolAccountType: string = 'work_school'; - private static MicrosoftAccountType: string = 'microsoft'; - private static AadCommonTenant: string = 'common'; + private static readonly CONFIGURATION_SECTION = 'accounts.azure.auth'; + private readonly authMappings = new Map(); + private initComplete: Deferred; + private initCompletePromise: Promise = new Promise((resolve, reject) => this.initComplete = { resolve, reject }); - // MEMBER VARIABLES //////////////////////////////////////////////////// - private _autoOAuthCancelled: boolean; - private _commonAuthorityUrl: string; - private _inProgressAutoOAuth: InProgressAutoOAuth; - private _isInitialized: boolean; - - constructor(private _metadata: AzureAccountProviderMetadata, private _tokenCache: TokenCache) { - this._autoOAuthCancelled = false; - this._inProgressAutoOAuth = null; - this._isInitialized = false; - - this._commonAuthorityUrl = url.resolve(this._metadata.settings.host, AzureAccountProvider.AadCommonTenant); - } - - // PUBLIC METHODS ////////////////////////////////////////////////////// - public autoOAuthCancelled(): Thenable { - return this.doIfInitialized(() => this.cancelAutoOAuth()); - } - - /** - * Clears all tokens that belong to the given account from the token cache - * @param accountKey Key identifying the account to delete tokens for - * @returns Promise to clear requested tokens from the token cache - */ - public clear(accountKey: azdata.AccountKey): Thenable { - return this.doIfInitialized(() => this.clearAccountTokens(accountKey)); - } - - /** - * Clears the entire token cache. Invoked by command palette action. - * @returns Promise to clear the token cache - */ - public clearTokenCache(): Thenable { - return this._tokenCache.clear(); - } - - public getSecurityToken(account: AzureAccount, resource: azdata.AzureResource): Thenable { - return this.doIfInitialized(() => this.getAccessTokens(account, resource)); - } - - public initialize(restoredAccounts: azdata.Account[]): Thenable { - let self = this; - - let rehydrationTasks: Thenable[] = []; - for (let account of restoredAccounts) { - // Purge any invalid accounts - if (!account) { - continue; - } - - // Refresh the contextual logo based on whether the account is a MS account - account.displayInfo.accountType = account.properties.isMsAccount - ? AzureAccountProvider.MicrosoftAccountType - : AzureAccountProvider.WorkSchoolAccountType; - - // Attempt to get fresh tokens. If this fails then the account is stale. - // NOTE: Based on ADAL implementation, getting tokens should use the refresh token if necessary - let task = this.getAccessTokens(account, azdata.AzureResource.ResourceManagement) - .then( - () => { - return account; - }, - () => { - account.isStale = true; - return account; - } - ); - rehydrationTasks.push(task); - } - - // Collect the rehydration tasks and mark the provider as initialized - return Promise.all(rehydrationTasks) - .then(accounts => { - self._isInitialized = true; - return accounts; - }); - } - - public prompt(): Thenable { - return this.doIfInitialized(() => this.signIn(true)); - } - - public refresh(account: AzureAccount): Thenable { - return this.doIfInitialized(() => this.signIn(false)); - } - - // PRIVATE METHODS ///////////////////////////////////////////////////// - private cancelAutoOAuth(): Promise { - let self = this; - - if (!this._inProgressAutoOAuth) { - console.warn('Attempted to cancel auto OAuth when auto OAuth is not in progress!'); - return Promise.resolve(); - } - - // Indicate oauth was cancelled by the user - let inProgress = self._inProgressAutoOAuth; - self._autoOAuthCancelled = true; - self._inProgressAutoOAuth = null; - - // Use the auth context that was originally used to open the polling request, and cancel the polling - let context = inProgress.context; - context.cancelRequestToGetTokenWithDeviceCode(inProgress.userCodeInfo, err => { - // Callback is only called in failure scenarios. - if (err) { - console.warn(`Error while cancelling auto OAuth: ${err}`); + constructor( + metadata: AzureAccountProviderMetadata, + tokenCache: SimpleTokenCache, + context: vscode.ExtensionContext + ) { + vscode.workspace.onDidChangeConfiguration((changeEvent) => { + const impact = changeEvent.affectsConfiguration(AzureAccountProvider.CONFIGURATION_SECTION); + if (impact === true) { + this.handleAuthMapping(metadata, tokenCache, context); } }); + this.handleAuthMapping(metadata, tokenCache, context); + } + + clearTokenCache(): Thenable { + return this.getAuthMethod().deleteAllCache(); + } + + private handleAuthMapping(metadata: AzureAccountProviderMetadata, tokenCache: SimpleTokenCache, context: vscode.ExtensionContext) { + this.authMappings.clear(); + const configuration = vscode.workspace.getConfiguration(AzureAccountProvider.CONFIGURATION_SECTION); + + const codeGrantMethod: boolean = configuration.get('codeGrant'); + const deviceCodeMethod: boolean = configuration.get('deviceCode'); + + if (codeGrantMethod === true) { + this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, context)); + } + if (deviceCodeMethod === true) { + this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, context)); + } + } + + private getAuthMethod(account?: azdata.Account): AzureAuth { + if (this.authMappings.size === 1) { + return this.authMappings.values().next().value; + } + + const authType: AzureAuthType = account?.properties?.azureAuthType; + if (authType) { + return this.authMappings.get(authType); + } else { + return this.authMappings.get(AzureAuthType.AuthCodeGrant); + } + } + + initialize(storedAccounts: azdata.Account[]): Thenable { + return this._initialize(storedAccounts); + } + + private async _initialize(storedAccounts: azdata.Account[]): Promise { + const accounts: azdata.Account[] = []; + for (let account of storedAccounts) { + const azureAuth = this.getAuthMethod(account); + if (!azureAuth) { + account.isStale = true; + accounts.push(account); + } else { + accounts.push(await azureAuth.refreshAccess(account)); + } + } + this.initComplete.resolve(); + return accounts; + } + + + getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable { + return this._getSecurityToken(account, resource); + } + + private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise { + await this.initCompletePromise; + const azureAuth = this.getAuthMethod(undefined); + return azureAuth?.getSecurityToken(account, resource); + } + + prompt(): Thenable { + return this._prompt(); + } + + private async _prompt(): Promise { + const noAuthSelected = localize('azure.NoAuthMethod.Selected', "No Azure auth method selected. You must select what method of authentication you want to use."); + const noAuthAvailable = localize('azure.NoAuthMethod.Available', "No Azure auth method available. You must enable the auth methods in ADS configuration."); + + await this.initCompletePromise; + class Option implements vscode.QuickPickItem { + public readonly label: string; + constructor(public readonly azureAuth: AzureAuth) { + this.label = azureAuth.userFriendlyName; + } + } + + if (this.authMappings.size === 0) { + console.log('No auth method was enabled.'); + await vscode.window.showErrorMessage(noAuthAvailable); + return { canceled: true }; + } + + if (this.authMappings.size === 1) { + return this.getAuthMethod(undefined).login(); + } + + const options: Option[] = []; + this.authMappings.forEach((azureAuth) => { + options.push(new Option(azureAuth)); + }); + + const pick = await vscode.window.showQuickPick(options, { canPickMany: false }); + + if (!pick) { + console.log('No auth method was selected.'); + await vscode.window.showErrorMessage(noAuthSelected); + return { canceled: true }; + } + + return pick.azureAuth.login(); + } + + refresh(account: azdata.Account): Thenable { + return this.prompt(); + } + + clear(accountKey: azdata.AccountKey): Thenable { + return this._clear(accountKey); + } + + private async _clear(accountKey: azdata.AccountKey): Promise { + await this.initCompletePromise; + await this.getAuthMethod(undefined)?.clearCredentials(accountKey); + } + + autoOAuthCancelled(): Thenable { + this.authMappings.forEach(val => val.autoOAuthCancelled()); return Promise.resolve(); } - - private async clearAccountTokens(accountKey: azdata.AccountKey): Promise { - // Put together a query to look up any tokens associated with the account key - let query = { userId: accountKey.accountId }; - - // 1) Look up the tokens associated with the query - // 2) Remove them - let results = await this._tokenCache.findThenable(query); - this._tokenCache.removeThenable(results); - } - - private doIfInitialized(op: () => Promise): Promise { - return this._isInitialized - ? op() - : Promise.reject(localize('accountProviderNotInitialized', "Account provider not initialized, cannot perform action")); - } - - private getAccessTokens(account: AzureAccount, resource: azdata.AzureResource): Promise { - let self = this; - - const resourceIdMap = new Map([ - [azdata.AzureResource.ResourceManagement, self._metadata.settings.armResource.id], - [azdata.AzureResource.Sql, self._metadata.settings.sqlResource.id], - [azdata.AzureResource.OssRdbms, self._metadata.settings.ossRdbmsResource.id], - [azdata.AzureResource.AzureKeyVault, self._metadata.settings.azureKeyVaultResource.id] - ]); - - let accessTokenPromises: Thenable[] = []; - let tokenCollection: AzureAccountSecurityTokenCollection = {}; - for (let tenant of account.properties.tenants) { - let promise = new Promise((resolve, reject) => { - let authorityUrl = url.resolve(self._metadata.settings.host, tenant.id); - let context = new adal.AuthenticationContext(authorityUrl, null, self._tokenCache); - - context.acquireToken( - resourceIdMap.get(resource), - tenant.userId, - self._metadata.settings.clientId, - (error: Error, response: adal.TokenResponse | adal.ErrorResponse) => { - // Handle errors first - if (error) { - // TODO: We'll assume for now that the account is stale, though that might not be accurate - account.isStale = true; - azdata.accounts.accountUpdated(account); - - reject(error); - return; - } - - // We know that the response was not an error - let tokenResponse = response; - - // Generate a token object and add it to the collection - tokenCollection[tenant.id] = { - expiresOn: tokenResponse.expiresOn, - resource: tokenResponse.resource, - token: tokenResponse.accessToken, - tokenType: tokenResponse.tokenType - }; - resolve(); - } - ); - }); - accessTokenPromises.push(promise); - } - - // Wait until all the tokens have been acquired then return the collection - return Promise.all(accessTokenPromises) - .then(() => tokenCollection); - } - - private getDeviceLoginUserCode(): Thenable { - let self = this; - - // Create authentication context and acquire user code - return new Promise((resolve, reject) => { - let context = new adal.AuthenticationContext(self._commonAuthorityUrl, null, self._tokenCache); - context.acquireUserCode(self._metadata.settings.signInResourceId, self._metadata.settings.clientId, vscode.env.language, - (err, response) => { - if (err) { - reject(err); - } else { - let result: InProgressAutoOAuth = { - context: context, - userCodeInfo: response - }; - - resolve(result); - } - } - ); - }); - } - - private getDeviceLoginToken(oAuth: InProgressAutoOAuth, isAddAccount: boolean): Thenable { - let self = this; - - // 1) Open the auto OAuth dialog - // 2) Begin the acquiring token polling - // 3) When that completes via callback, close the auto oauth - let title = isAddAccount ? - localize('addAccount', "Add {0} account", self._metadata.displayName) : - localize('refreshAccount', "Refresh {0} account", self._metadata.displayName); - return azdata.accounts.beginAutoOAuthDeviceCode(self._metadata.id, title, oAuth.userCodeInfo.message, oAuth.userCodeInfo.userCode, oAuth.userCodeInfo.verificationUrl) - .then(() => { - return new Promise((resolve, reject) => { - let context = oAuth.context; - context.acquireTokenWithDeviceCode(self._metadata.settings.signInResourceId, self._metadata.settings.clientId, oAuth.userCodeInfo, - (err, response) => { - if (err) { - if (self._autoOAuthCancelled) { - let result: azdata.PromptFailedResult = { canceled: true }; - // Auto OAuth was cancelled by the user, indicate this with the error we return - resolve(result); - } else { - // Auto OAuth failed for some other reason - azdata.accounts.endAutoOAuthDeviceCode(); - reject(err); - } - } else { - azdata.accounts.endAutoOAuthDeviceCode(); - resolve(response); - } - - } - ); - }); - }); - } - - private getTenants(userId: string, homeTenant: string): Thenable { - let self = this; - - // 1) Get a token we can use for looking up the tenant IDs - // 2) Send a request to the ARM endpoint (the root management API) to get the list of tenant IDs - // 3) For all the tenants - // b) Get a token we can use for the AAD Graph API - // a) Get the display name of the tenant - // c) create a tenant object - // 4) Sort to make sure the "home tenant" is the first tenant on the list - return this.getToken(userId, AzureAccountProvider.AadCommonTenant, this._metadata.settings.armResource.id) - .then((armToken: adal.TokenResponse) => { - let tenantUri = url.resolve(self._metadata.settings.armResource.endpoint, 'tenants?api-version=2015-01-01'); - return self.makeWebRequest(armToken, tenantUri); - }) - .then((tenantResponse: any[]) => { - let promises: Thenable[] = tenantResponse.map(value => { - return self.getToken(userId, value.tenantId, self._metadata.settings.graphResource.id) - .then((graphToken: adal.TokenResponse) => { - let tenantDetailsUri = url.resolve(self._metadata.settings.graphResource.endpoint, value.tenantId + '/'); - tenantDetailsUri = url.resolve(tenantDetailsUri, 'tenantDetails?api-version=2013-04-05'); - return self.makeWebRequest(graphToken, tenantDetailsUri); - }) - .then((tenantDetails: any) => { - return { - id: value.tenantId, - userId: userId, - displayName: tenantDetails.length && tenantDetails[0].displayName - ? tenantDetails[0].displayName - : localize('azureWorkAccountDisplayName', "Work or school account") - }; - }); - }); - - return Promise.all(promises); - }) - .then((tenants: Tenant[]) => { - let homeTenantIndex = tenants.findIndex(tenant => tenant.id === homeTenant); - if (homeTenantIndex >= 0) { - let homeTenant = tenants.splice(homeTenantIndex, 1); - tenants.unshift(homeTenant[0]); - } - return tenants; - }); - } - - /** - * Retrieves a token for the given user ID for the specific tenant ID. If the token can, it - * will be retrieved from the cache as per the ADAL API. AFAIK, the ADAL API will also utilize - * the refresh token if there aren't any unexpired tokens to use. - * @param userId ID of the user to get a token for - * @param tenantId Tenant to get the token for - * @param resourceId ID of the resource the token will be good for - * @returns Promise to return a token. Rejected if retrieving the token fails. - */ - private getToken(userId: string, tenantId: string, resourceId: string): Thenable { - let self = this; - - return new Promise((resolve, reject) => { - let authorityUrl = url.resolve(self._metadata.settings.host, tenantId); - let context = new adal.AuthenticationContext(authorityUrl, null, self._tokenCache); - context.acquireToken(resourceId, userId, self._metadata.settings.clientId, - (error: Error, response: adal.TokenResponse | adal.ErrorResponse) => { - if (error) { - reject(error); - } else { - resolve(response); - } - } - ); - }); - } - - /** - * Performs a web request using the provided bearer token - * @param accessToken Bearer token for accessing the provided URI - * @param uri URI to access - * @returns Promise to return the deserialized body of the request. Rejected if error occurred. - */ - private makeWebRequest(accessToken: adal.TokenResponse, uri: string): Thenable { - return new Promise((resolve, reject) => { - // Setup parameters for the request - // NOTE: setting json true means the returned object will be deserialized - let params = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken.accessToken}` - }, - json: true - }; - - // Setup the callback to resolve/reject this promise - const callback: request.RequestCallback = (error, response, body: { error: any; value: any; }) => { - if (error || body.error) { - reject(error || JSON.stringify(body.error)); - } else { - resolve(body.value); - } - }; - - // Make the request - request.get(uri, params, callback); - }); - } - - private isPromptFailed(value: adal.TokenResponse | azdata.PromptFailedResult): value is azdata.PromptFailedResult { - return value && (value).canceled; - } - - private async signIn(isAddAccount: boolean): Promise { - // 1) Get the user code for this login - // 2) Get an access token from the device code - // 3) Get the list of tenants - // 4) Generate the AzureAccount object and return it - let tokenResponse: adal.TokenResponse = null; - let result: InProgressAutoOAuth = await this.getDeviceLoginUserCode(); - this._autoOAuthCancelled = false; - this._inProgressAutoOAuth = result; - let response: adal.TokenResponse | azdata.PromptFailedResult = await this.getDeviceLoginToken(this._inProgressAutoOAuth, isAddAccount); - if (this.isPromptFailed(response)) { - return response; - } - tokenResponse = response; - this._autoOAuthCancelled = false; - this._inProgressAutoOAuth = null; - let tenants: Tenant[] = await this.getTenants(tokenResponse.userId, tokenResponse.userId); - // Figure out where we're getting the identity from - let identityProvider = tokenResponse.identityProvider; - if (identityProvider) { - identityProvider = identityProvider.toLowerCase(); - } - - // Determine if this is a microsoft account - let msa = identityProvider && ( - identityProvider.indexOf('live.com') !== -1 || - identityProvider.indexOf('live-int.com') !== -1 || - 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 = (tokenResponse.givenName && tokenResponse.familyName) - ? `${tokenResponse.givenName} ${tokenResponse.familyName}` - : tokenResponse.userId; - - // Calculate the home tenant display name to use for the contextual display name - let contextualDisplayName = msa - ? localize('microsoftAccountDisplayName', "Microsoft Account") - : tenants[0].displayName; - - // Calculate the account type - let accountType = msa - ? AzureAccountProvider.MicrosoftAccountType - : AzureAccountProvider.WorkSchoolAccountType; - - return { - key: { - providerId: this._metadata.id, - accountId: tokenResponse.userId - }, - name: tokenResponse.userId, - displayInfo: { - accountType: accountType, - userId: tokenResponse.userId, - contextualDisplayName: contextualDisplayName, - displayName: displayName - }, - properties: { - providerSettings: this._metadata, - isMsAccount: msa, - tenants: tenants - }, - isStale: false - }; - } -} - -interface InProgressAutoOAuth { - context: adal.AuthenticationContext; - userCodeInfo: adal.UserCodeInfo; } diff --git a/extensions/azurecore/src/account-provider/azureAccountProvider2.ts b/extensions/azurecore/src/account-provider/azureAccountProvider2.ts deleted file mode 100644 index 9b5f667d58..0000000000 --- a/extensions/azurecore/src/account-provider/azureAccountProvider2.ts +++ /dev/null @@ -1,509 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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'; -import { promises as fs } from 'fs'; -import * as path from 'path'; - -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, private _context: vscode.ExtensionContext) { - this.commonAuthorityUrl = url.resolve(this.metadata.settings.host, AzureAccountProvider.AadCommonTenant); - } - // interface method - clearTokenCache(): Thenable { - return this._tokenCache.clear(); - } - - // 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) => { - if (err || !result) { - const msg = localize('azure.tokenCacheFail', "Unexpected error adding token to cache: {0}", err.message); - vscode.window.showErrorMessage(msg); - console.log(err); - } - }); - - 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], - [azdata.AzureResource.OssRdbms, this.metadata.settings.ossRdbmsResource.id], - [azdata.AzureResource.AzureKeyVault, this.metadata.settings.azureKeyVaultResource.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 mediaPath = path.join(this._context.extensionPath, 'media'); - - // Utility function - const sendFile = async (res: http.ServerResponse, filePath: string, contentType: string): Promise => { - let fileContents; - try { - fileContents = await fs.readFile(filePath); - } catch (ex) { - console.error(ex); - res.writeHead(200); - res.end(); - return; - } - - res.writeHead(200, { - 'Content-Length': fileContents.length, - 'Content-Type': contentType - }); - - res.end(fileContents); - }; - - 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 authCallback = (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; - } - - - sendFile(res, path.join(mediaPath, 'landing.html'), 'text/html; charset=utf-8').catch(console.error); - this.handleAuthentication(code).catch((e) => console.error(e)); - }; - - const css = (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => { - sendFile(res, path.join(mediaPath, 'landing.css'), 'text/css; charset=utf-8').catch(console.error); - }; - - const svg = (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => { - sendFile(res, path.join(mediaPath, 'SignIn.svg'), 'image/svg+xml').catch(console.error); - }; - - pathMappings.set('/signin', initialSignIn); - pathMappings.set('/callback', authCallback); - pathMappings.set('/landing.css', css); - pathMappings.set('/SignIn.svg', svg); - } - - 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 }) => { - let graphToken: TokenResponse; - - try { - graphToken = await this.getToken(userId, value.tenantId, this.metadata.settings.graphResource.id); - } catch (ex) { - const msg = localize('azure.authFail', "Your authentication to the tenant {0} failed: {1}", value.tenantId, ex); - vscode.window.showErrorMessage(msg); - console.log(msg); - return undefined; - } - - 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: tenantDetails.length > 0 && tenantDetails[0].displayName - ? tenantDetails[0].displayName - : localize('azureWorkAccountDisplayName', "Work or school account") - } as Tenant; - }); - - let tenants = await Promise.all(promises); - - tenants = tenants.filter(t => t !== undefined); - if (tenants.length === 0) { - const msg = localize('azure.noTenants', "Failed to add account. No Azure tenants."); - vscode.window.showErrorMessage(msg); - throw new Error(msg); - } - - if (homeTenant) { - 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 { - let token: TokenResponse; - token = await this.getTokenWithAuthCode(code, AzureAccountProvider.redirectUrlAAD); - const tenants = await this.getTenants(token.userId, token.tenantId); - 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.log('undefined request ', reqUrl.pathname, 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 613f94bcb1..49e754b304 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProviderService.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts @@ -6,11 +6,10 @@ import * as azdata from 'azdata'; import * as events from 'events'; import * as nls from 'vscode-nls'; -import * as path from 'path'; import * as vscode from 'vscode'; -import CredentialServiceTokenCache from './tokenCache'; +import { SimpleTokenCache } from './simpleTokenCache'; import providerSettings from './providerSettings'; -import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider2'; +import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider'; import { AzureAccountProviderMetadata, ProviderSettings } from './interfaces'; import * as loc from '../localizedConstants'; @@ -19,7 +18,7 @@ let localize = nls.loadMessageBundle(); export class AzureAccountProviderService implements vscode.Disposable { // CONSTANTS /////////////////////////////////////////////////////////////// private static CommandClearTokenCache = 'accounts.clearTokenCache'; - private static ConfigurationSection = 'accounts.azure'; + private static ConfigurationSection = 'accounts.azure.cloud'; private static CredentialNamespace = 'azureAccountProviderCredentials'; // MEMBER VARIABLES //////////////////////////////////////////////////////// @@ -55,10 +54,13 @@ export class AzureAccountProviderService implements vscode.Disposable { // 2c) Perform an initial config change handling return azdata.credentials.getProvider(AzureAccountProviderService.CredentialNamespace) .then(credProvider => { - self._credentialProvider = credProvider; + this._credentialProvider = credProvider; - self._context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(self.onDidChangeConfiguration, self)); - self.onDidChangeConfiguration(); + this._context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { + this._configChangePromiseChain = this.onDidChangeConfiguration(); + }, this)); + + this._configChangePromiseChain = this.onDidChangeConfiguration(); return true; }); } @@ -85,82 +87,64 @@ export class AzureAccountProviderService implements vscode.Disposable { }); } - private onDidChangeConfiguration(): void { - let self = this; - + private async onDidChangeConfiguration(): Promise { // Add a new change processing onto the existing promise change - this._configChangePromiseChain = this._configChangePromiseChain.then(() => { - // Grab the stored config and the latest config - let newConfig = vscode.workspace.getConfiguration(AzureAccountProviderService.ConfigurationSection); - let oldConfig = self._currentConfig; - self._currentConfig = newConfig; + await this._configChangePromiseChain; + // Grab the stored config and the latest config + let newConfig = vscode.workspace.getConfiguration(AzureAccountProviderService.ConfigurationSection); + let oldConfig = this._currentConfig; + this._currentConfig = newConfig; - // Determine what providers need to be changed - let providerChanges: Thenable[] = []; - for (let provider of providerSettings) { - // If the old config doesn't exist, then assume everything was disabled - // There will always be a new config value - let oldConfigValue = oldConfig - ? oldConfig.get(provider.configKey) - : false; - let newConfigValue = newConfig.get(provider.configKey); + // Determine what providers need to be changed + let providerChanges: Promise[] = []; + for (let provider of providerSettings) { + // If the old config doesn't exist, then assume everything was disabled + // There will always be a new config value + let oldConfigValue = oldConfig + ? oldConfig.get(provider.configKey) + : false; + let newConfigValue = newConfig.get(provider.configKey); - // Case 1: Provider config has not changed - do nothing - if (oldConfigValue === newConfigValue) { - continue; - } - - // Case 2: Provider was enabled and is now disabled - unregister provider - if (oldConfigValue && !newConfigValue) { - providerChanges.push(self.unregisterAccountProvider(provider)); - } - - // Case 3: Provider was disabled and is now enabled - register provider - if (!oldConfigValue && newConfigValue) { - providerChanges.push(self.registerAccountProvider(provider)); - } + // Case 1: Provider config has not changed - do nothing + if (oldConfigValue === newConfigValue) { + continue; } - // Process all the changes before continuing - return Promise.all(providerChanges); - }).then(null, () => { return Promise.resolve(); }); + // Case 2: Provider was enabled and is now disabled - unregister provider + if (oldConfigValue && !newConfigValue) { + providerChanges.push(this.unregisterAccountProvider(provider)); + } + + // Case 3: Provider was disabled and is now enabled - register provider + if (!oldConfigValue && newConfigValue) { + providerChanges.push(this.registerAccountProvider(provider)); + } + } + + // Process all the changes before continuing + await Promise.all(providerChanges); } - 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 as AzureAccountProviderMetadata, tokenCache, this._context); - self._accountProviders[provider.metadata.id] = accountProvider; - self._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider); - resolve(); - } catch (e) { - console.error(`Failed to register account provider: ${e}`); - reject(e); - } - }); + private async registerAccountProvider(provider: ProviderSettings): Promise { + try { + let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`; + let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, false, this._credentialProvider); + await simpleTokenCache.init(); + let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata, simpleTokenCache, this._context); + this._accountProviders[provider.metadata.id] = accountProvider; + this._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider); + } catch (e) { + console.error(`Failed to register account provider: ${e}`); + } } - private unregisterAccountProvider(provider: ProviderSettings): Thenable { - let self = this; - - return new Promise((resolve, reject) => { - try { - self._accountDisposals[provider.metadata.id].dispose(); - delete self._accountProviders[provider.metadata.id]; - delete self._accountDisposals[provider.metadata.id]; - resolve(); - } catch (e) { - console.error(`Failed to unregister account provider: ${e}`); - reject(e); - } - }); + private async unregisterAccountProvider(provider: ProviderSettings): Promise { + try { + this._accountDisposals[provider.metadata.id].dispose(); + delete this._accountProviders[provider.metadata.id]; + delete this._accountDisposals[provider.metadata.id]; + } catch (e) { + console.error(`Failed to unregister account provider: ${e}`); + } } } diff --git a/extensions/azurecore/src/account-provider/interfaces.ts b/extensions/azurecore/src/account-provider/interfaces.ts index 0f56b5cb3a..edf50be2ff 100644 --- a/extensions/azurecore/src/account-provider/interfaces.ts +++ b/extensions/azurecore/src/account-provider/interfaces.ts @@ -23,12 +23,17 @@ export interface Tenant { * Identifier of the user in the tenant */ userId: string; + + /** + * The category the user has set their tenant to (e.g. Home Tenant) + */ + tenantCategory: string; } /** * Represents a resource exposed by an Azure Active Directory */ -interface Resource { +export interface Resource { /** * Identifier of the resource */ @@ -38,6 +43,11 @@ interface Resource { * Endpoint url used to access the resource */ endpoint: string; + + /** + * Resource ID for azdata + */ + azureResourceId?: azdata.AzureResource } /** @@ -101,6 +111,8 @@ interface Settings { * Redirect URI that is used to signify the end of the interactive aspect of sign it */ redirectUri?: string; + + scopes?: string[] } /** @@ -128,10 +140,20 @@ export interface AzureAccountProviderMetadata extends azdata.AccountProviderMeta settings: Settings; } +export enum AzureAuthType { + AuthCodeGrant = 0, + DeviceCode = 1 +} + /** * Properties specific to an Azure account */ interface AzureAccountProperties { + /** + * Auth type of azure used to authenticate this account. + */ + azureAuthType?: AzureAuthType + providerSettings: AzureAccountProviderMetadata; /** * Whether or not the account is a Microsoft account @@ -142,6 +164,17 @@ interface AzureAccountProperties { * A list of tenants (aka directories) that the account belongs to */ tenants: Tenant[]; + + /** + * A list of subscriptions the user belongs to + */ + subscriptions?: Subscription[]; +} + +export interface Subscription { + id: string, + tenantId: string, + displayName: string } /** @@ -184,3 +217,8 @@ export interface AzureAccountSecurityToken { * an access token. The list of tenants correspond to the tenants in the account properties. */ export type AzureAccountSecurityTokenCollection = { [tenantId: string]: AzureAccountSecurityToken }; + +export interface Deferred { + resolve: (result: T | Promise) => void; + reject: (reason: any) => void; +} diff --git a/extensions/azurecore/src/account-provider/providerSettings.ts b/extensions/azurecore/src/account-provider/providerSettings.ts index 4476f33013..cd8e28615f 100644 --- a/extensions/azurecore/src/account-provider/providerSettings.ts +++ b/extensions/azurecore/src/account-provider/providerSettings.ts @@ -5,6 +5,7 @@ import * as nls from 'vscode-nls'; import { ProviderSettings } from './interfaces'; +import { AzureResource } from 'azdata'; const localize = nls.loadMessageBundle(); @@ -12,32 +13,40 @@ const publicAzureSettings: ProviderSettings = { configKey: 'enablePublicCloud', metadata: { displayName: localize('publicCloudDisplayName', "Azure"), - id: 'azurePublicCloud', + id: 'azure_publicCloud', settings: { host: 'https://login.microsoftonline.com/', clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255', signInResourceId: 'https://management.core.windows.net/', graphResource: { - id: 'https://graph.windows.net/', - endpoint: 'https://graph.windows.net' + id: 'graph', + endpoint: 'https://graph.microsoft.com', + azureResourceId: AzureResource.Graph }, armResource: { - id: 'https://management.core.windows.net/', - endpoint: 'https://management.azure.com' + id: 'arm', + endpoint: 'https://management.azure.com', + azureResourceId: AzureResource.ResourceManagement }, sqlResource: { - id: 'https://database.windows.net/', - endpoint: 'https://database.windows.net' + id: 'sql', + endpoint: 'https://database.windows.net', + azureResourceId: AzureResource.Sql }, ossRdbmsResource: { - id: 'https://ossrdbms-aad.database.windows.net', - endpoint: 'https://ossrdbms-aad.database.windows.net' + id: 'ossrdbms', + endpoint: 'https://ossrdbms-aad.database.windows.net', + azureResourceId: AzureResource.OssRdbms }, azureKeyVaultResource: { id: 'https://vault.azure.net', endpoint: 'https://vault.azure.net' }, - redirectUri: 'http://localhost/redirect' + redirectUri: 'https://vscode-redirect.azurewebsites.net/', + scopes: [ + 'openid', 'email', 'profile', 'offline_access', + 'https://management.azure.com/user_impersonation', + ] } } }; @@ -47,24 +56,40 @@ const usGovAzureSettings: ProviderSettings = { configKey: 'enableUsGovCloud', metadata: { displayName: localize('usGovCloudDisplayName', "Azure (US Government)"), - id: 'usGovAzureCloud', + id: 'azure_usGovtCloud', settings: { - host: 'https://login.microsoftonline.us', - clientId: 'TBD', + host: 'https://login.microsoftonline.us/', + clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255', signInResourceId: 'https://management.core.usgovcloudapi.net/', graphResource: { - id: 'https://graph.usgovcloudapi.net/', - endpoint: 'https://graph.usgovcloudapi.net' + id: 'graph', + endpoint: 'https://graph.windows.net', + azureResourceId: AzureResource.Graph }, armResource: { - id: 'https://management.core.usgovcloudapi.net/', - endpoint: 'https://management.usgovcloudapi.net' + id: 'arm', + endpoint: 'https://management.usgovcloudapi.net', + azureResourceId: AzureResource.ResourceManagement + }, + sqlResource: { + id: 'sql', + endpoint: 'https://database.usgovcloudapi.net', + azureResourceId: AzureResource.Sql + }, + ossRdbmsResource: { + id: 'ossrdbms', + endpoint: 'https://ossrdbms-aad.database.usgovcloudapi.net', + azureResourceId: AzureResource.OssRdbms }, azureKeyVaultResource: { id: 'https://vault.usgovcloudapi.net', endpoint: 'https://vault.usgovcloudapi.net' }, - redirectUri: 'http://localhost/redirect' + redirectUri: 'https://vscode-redirect.azurewebsites.net/', + scopes: [ + 'openid', 'email', 'profile', 'offline_access', + 'https://management.usgovcloudapi.net/user_impersonation' + ] } } }; @@ -74,10 +99,10 @@ const germanyAzureSettings: ProviderSettings = { configKey: 'enableGermanyCloud', metadata: { displayName: localize('germanyCloud', "Azure (Germany)"), - id: 'germanyAzureCloud', + id: 'azure_germanyCloud', settings: { host: 'https://login.microsoftazure.de/', - clientId: 'TBD', + clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255', signInResourceId: 'https://management.core.cloudapi.de/', graphResource: { id: 'https://graph.cloudapi.de/', @@ -91,7 +116,7 @@ const germanyAzureSettings: ProviderSettings = { id: 'https://vault.microsoftazure.de', endpoint: 'https://vault.microsoftazure.de' }, - redirectUri: 'http://localhost/redirect' + redirectUri: 'https://vscode-redirect.azurewebsites.net/' } } }; @@ -100,10 +125,10 @@ const chinaAzureSettings: ProviderSettings = { configKey: 'enableChinaCloud', metadata: { displayName: localize('chinaCloudDisplayName', "Azure (China)"), - id: 'chinaAzureCloud', + id: 'azure_chinaCloud', settings: { host: 'https://login.chinacloudapi.cn/', - clientId: 'TBD', + clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255', signInResourceId: 'https://management.core.chinacloudapi.cn/', graphResource: { id: 'https://graph.chinacloudapi.cn/', @@ -117,7 +142,8 @@ const chinaAzureSettings: ProviderSettings = { id: 'https://vault.azure.cn', endpoint: 'https://vault.azure.cn' }, - redirectUri: 'http://localhost/redirect' + redirectUri: 'https://vscode-redirect.azurewebsites.net/' + } } }; diff --git a/extensions/azurecore/src/account-provider/simpleTokenCache.ts b/extensions/azurecore/src/account-provider/simpleTokenCache.ts new file mode 100644 index 0000000000..f9a6e5077e --- /dev/null +++ b/extensions/azurecore/src/account-provider/simpleTokenCache.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as keytarType from 'keytar'; +import { join, parse } from 'path'; +import { FileDatabase } from './utils/fileDatabase'; +import * as crypto from 'crypto'; +import * as azdata from 'azdata'; + +function getSystemKeytar(): Keytar | undefined { + try { + return require('keytar'); + } catch (err) { + console.log(err); + } + + return undefined; +} + +export type MultipleAccountsResponse = { account: string, password: string }[]; + +const separator = 'ยง'; + +async function getFileKeytar(filePath: string, credentialService: azdata.CredentialProvider): Promise { + const fileName = parse(filePath).base; + const iv = await credentialService.readCredential(`${fileName}-iv`); + const key = await credentialService.readCredential(`${fileName}-key`); + let ivBuffer: Buffer; + let keyBuffer: Buffer; + if (!iv?.password || !key?.password) { + ivBuffer = crypto.randomBytes(16); + keyBuffer = crypto.randomBytes(32); + try { + await credentialService.saveCredential(`${fileName}-iv`, ivBuffer.toString('hex')); + await credentialService.saveCredential(`${fileName}-key`, keyBuffer.toString('hex')); + } catch (ex) { + console.log(ex); + } + } else { + ivBuffer = Buffer.from(iv.password, 'hex'); + keyBuffer = Buffer.from(key.password, 'hex'); + } + + const fileSaver = async (content: string): Promise => { + const cipherIv = crypto.createCipheriv('aes-256-gcm', keyBuffer, ivBuffer); + return `${cipherIv.update(content, 'utf8', 'hex')}${cipherIv.final('hex')}%${cipherIv.getAuthTag().toString('hex')}`; + }; + + const fileOpener = async (content: string): Promise => { + const decipherIv = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer); + + const split = content.split('%'); + if (split.length !== 2) { + throw new Error('File didn\'t contain the auth tag.'); + } + decipherIv.setAuthTag(Buffer.from(split[1], 'hex')); + + return `${decipherIv.update(split[0], 'hex', 'utf8')}${decipherIv.final('utf8')}`; + }; + + const db = new FileDatabase(filePath, fileOpener, fileSaver); + await db.initialize(); + + const fileKeytar: Keytar = { + async getPassword(service: string, account: string): Promise { + return db.get(`${service}${separator}${account}`); + }, + + async setPassword(service: string, account: string, password: string): Promise { + await db.set(`${service}${separator}${account}`, password); + }, + + async deletePassword(service: string, account: string): Promise { + await db.delete(`${service}${separator}${account}`); + return true; + }, + + async getPasswords(service: string): Promise { + const result = db.getPrefix(`${service}`); + if (!result) { + return []; + } + + return result.map(({ key, value }) => { + return { + account: key.split(separator)[1], + password: value + }; + }); + } + }; + return fileKeytar; +} + +export type Keytar = { + getPassword: typeof keytarType['getPassword']; + setPassword: typeof keytarType['setPassword']; + deletePassword: typeof keytarType['deletePassword']; + getPasswords: (service: string) => Promise; + findCredentials?: typeof keytarType['findCredentials']; +}; + +export class SimpleTokenCache { + private keytar: Keytar; + + constructor( + private serviceName: string, + private readonly userStoragePath: string, + private readonly forceFileStorage: boolean = false, + private readonly credentialService: azdata.CredentialProvider, + ) { + + } + + async init(): Promise { + this.serviceName = this.serviceName.replace(/-/g, '_'); + let keytar: Keytar; + if (this.forceFileStorage === false) { + keytar = getSystemKeytar(); + + // Override how findCredentials works + keytar.getPasswords = async (service: string): Promise => { + const [serviceName, accountPrefix] = service.split(separator); + if (serviceName === undefined || accountPrefix === undefined) { + throw new Error('Service did not have seperator: ' + service); + } + + const results = await keytar.findCredentials(serviceName); + return results.filter(({ account }) => { + return account.startsWith(accountPrefix); + }); + }; + } + if (!keytar) { + keytar = await getFileKeytar(join(this.userStoragePath, this.serviceName), this.credentialService); + } + this.keytar = keytar; + } + + async saveCredential(id: string, key: string): Promise { + if (key.length > 2500) { // Windows limitation + throw new Error('Key length is longer than 2500 chars'); + } + + if (id.includes(separator)) { + throw new Error('Separator included in ID'); + } + + try { + return await this.keytar.setPassword(this.serviceName, id, key); + } catch (ex) { + console.log(`Adding key failed: ${ex}`); + } + } + + async getCredential(id: string): Promise { + try { + const result = await this.keytar.getPassword(this.serviceName, id); + if (result === null) { + return undefined; + } + return result; + } catch (ex) { + console.log(`Getting key failed: ${ex}`); + return undefined; + } + } + + async clearCredential(id: string): Promise { + try { + return await this.keytar.deletePassword(this.serviceName, id); + } catch (ex) { + console.log(`Clearing key failed: ${ex}`); + return false; + } + } + + async findCredentials(prefix: string): Promise<{ account: string, password: string }[]> { + try { + return await this.keytar.getPasswords(`${this.serviceName}${separator}${prefix}`); + } catch (ex) { + console.log(`Finding credentials failed: ${ex}`); + return undefined; + } + } +} diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts index 52d8c0fa17..0c38923107 100644 --- a/extensions/azurecore/src/azureResource/commands.ts +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -120,6 +120,7 @@ export function registerAzureResourceCommands(appContext: AppContext, tree: Azur subscriptions.push(...await subscriptionService.getSubscriptions(accountNode.account, new TokenCredentials(token, tokenType))); } } catch (error) { + this.account.isStale = true; throw new AzureResourceCredentialError(localize('azure.resource.selectsubscriptions.credentialError', "Failed to get credential for account {0}. Please refresh the account.", this.account.key.accountId), error); } } diff --git a/extensions/azurecore/src/test/account-provider/auths/azureAuth.test.ts b/extensions/azurecore/src/test/account-provider/auths/azureAuth.test.ts new file mode 100644 index 0000000000..db47317606 --- /dev/null +++ b/extensions/azurecore/src/test/account-provider/auths/azureAuth.test.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as os from 'os'; +import 'mocha'; + +import { PromptFailedResult, AccountKey } from 'azdata'; +import { AzureAuth, AccessToken, RefreshToken, TokenClaims } from '../../../account-provider/auths/azureAuth'; +import { AzureAccount, AzureAuthType } from '../../../account-provider/interfaces'; +import providerSettings from '../../../account-provider/providerSettings'; +import { SimpleTokenCache } from '../../../account-provider/simpleTokenCache'; +import { CredentialsTestProvider } from '../../stubs/credentialsTestProvider'; + +class BasicAzureAuth extends AzureAuth { + public login(): Promise { + throw new Error('Method not implemented.'); + } + public autoOAuthCancelled(): Promise { + throw new Error('Method not implemented.'); + } +} + +let baseAuth: AzureAuth; + +const accountKey: AccountKey = { + accountId: 'SomeAccountKey', + providerId: 'providerId', +}; + +const accessToken: AccessToken = { + key: accountKey.accountId, + token: '123' +}; + +const refreshToken: RefreshToken = { + key: accountKey.accountId, + token: '321' +}; + +const resourceId = 'resource'; +const tenantId = 'tenant'; + +// These tests don't work on Linux systems because gnome-keyring doesn't like running on headless machines. +describe('AccountProvider.AzureAuth', function (): void { + beforeEach(async function (): Promise { + const tokenCache = new SimpleTokenCache('testTokenService', os.tmpdir(), true, new CredentialsTestProvider()); + await tokenCache.init(); + baseAuth = new BasicAzureAuth(providerSettings[0].metadata, tokenCache, undefined, AzureAuthType.AuthCodeGrant, 'Auth Code Grant'); + }); + + it('Basic token set and get', async function (): Promise { + await baseAuth.setCachedToken(accountKey, accessToken, refreshToken); + const result = await baseAuth.getCachedToken(accountKey); + + should(JSON.stringify(result.accessToken)).be.equal(JSON.stringify(accessToken)); + should(JSON.stringify(result.refreshToken)).be.equal(JSON.stringify(refreshToken)); + }); + + it('Token set and get with tenant and resource id', async function (): Promise { + await baseAuth.setCachedToken(accountKey, accessToken, refreshToken, resourceId, tenantId); + let result = await baseAuth.getCachedToken(accountKey, resourceId, tenantId); + + should(JSON.stringify(result.accessToken)).be.equal(JSON.stringify(accessToken)); + should(JSON.stringify(result.refreshToken)).be.equal(JSON.stringify(refreshToken)); + + await baseAuth.clearCredentials(accountKey); + result = await baseAuth.getCachedToken(accountKey, resourceId, tenantId); + should(result).be.undefined(); + }); + + it('Token set with resource ID and get without tenant and resource id', async function (): Promise { + await baseAuth.setCachedToken(accountKey, accessToken, refreshToken, resourceId, tenantId); + const result = await baseAuth.getCachedToken(accountKey); + + should(JSON.stringify(result)).be.undefined(); + should(JSON.stringify(result)).be.undefined(); + }); + + it('Create an account object', async function (): Promise { + const tokenClaims = { + idp: 'live.com', + name: 'TestAccount', + } as TokenClaims; + + const account = baseAuth.createAccount(tokenClaims, 'someKey', undefined); + + should(account.properties.azureAuthType).be.equal(AzureAuthType.AuthCodeGrant); + should(account.key.accountId).be.equal('someKey'); + should(account.properties.isMsAccount).be.equal(true); + }); +}); diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock index da280f4012..4367d2d680 100644 --- a/extensions/azurecore/yarn.lock +++ b/extensions/azurecore/yarn.lock @@ -68,6 +68,13 @@ dependencies: "@types/node" "*" +"@types/keytar@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.2.tgz#49ef917d6cbb4f19241c0ab50cd35097b5729b32" + integrity sha512-xtQcDj9ruGnMwvSu1E2BH4SFa5Dv2PvSPd0CKEBLN5hEj/v5YpXJY+B6hAfuKIbvEomD7vJTc/P1s1xPNh2kRw== + dependencies: + keytar "*" + "@types/mocha@^5.2.5": version "5.2.5" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" @@ -88,6 +95,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.45.tgz#4c49ba34106bc7dced77ff6bae8eb6543cde8351" integrity sha512-tGVTbA+i3qfXsLbq9rEq/hezaHY55QxQLeXQL2ejNgFAxxrgu8eMmYIOsRcl7hN1uTLVsKOOYacV/rcJM3sfgQ== +"@types/qs@^6.9.1": + version "6.9.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7" + integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw== + "@types/request@^2.48.1": version "2.48.1" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.1.tgz#e402d691aa6670fbbff1957b15f1270230ab42fa" @@ -164,6 +176,11 @@ ansi-red@^0.1.1: dependencies: ansi-wrap "0.1.0" +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" @@ -174,6 +191,19 @@ ansi-wrap@0.1.0: resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -263,6 +293,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -270,6 +305,15 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bl@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" + integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -293,6 +337,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce" + integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + callsite@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" @@ -308,11 +360,21 @@ charenc@~0.0.1: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + circular-json@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" @@ -335,6 +397,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -385,6 +452,18 @@ decache@^4.4.0: dependencies: callsite "^1.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -402,6 +481,16 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + diff@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -422,6 +511,13 @@ ecdsa-sig-formatter@1.0.10: dependencies: safe-buffer "^5.0.1" +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + es-abstract@^1.5.1: version "1.16.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" @@ -479,6 +575,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + extend-shallow@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071" @@ -546,6 +647,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -556,6 +662,20 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -563,6 +683,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -642,6 +767,11 @@ has-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has@^1.0.1, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -663,6 +793,11 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -676,6 +811,16 @@ inherits@2, inherits@~2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + is-buffer@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" @@ -696,6 +841,18 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -805,6 +962,14 @@ jws@3.x.x: jwa "^1.2.0" safe-buffer "^5.0.1" +keytar@*, keytar@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.4.0.tgz#71d8209e7dd2fe99008c243791350a6bd6ceab67" + integrity sha512-Ta0RtUmkq7un177SPgXKQ7FGfGDV4xvsV0cGNiWVEzash5U0wyOsXpwfrK2+Oq+hHvsvsbzIZUUuJPimm3avFw== + dependencies: + nan "2.14.0" + prebuild-install "5.3.3" + kind-of@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" @@ -844,6 +1009,11 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.40.0" +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -856,6 +1026,11 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -868,6 +1043,13 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@~0.5.1: dependencies: minimist "0.0.8" +mkdirp@^0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.3.tgz#5a514b7179259287952881e94410ec5465659f8c" + integrity sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg== + dependencies: + minimist "^1.2.5" + mocha-junit-reporter@^1.17.0: version "1.23.1" resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.1.tgz#ba11519c0b967f404e4123dd69bc4ba022ab0f12" @@ -914,11 +1096,33 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +nan@2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + neo-async@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== +node-abi@^2.7.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.15.0.tgz#51d55cc711bd9e4a24a572ace13b9231945ccb10" + integrity sha512-FeLpTS0F39U7hHZU1srAK4Vx+5AHNVOTP+hxBNQknR/54laTHSFIJkDWDqiquY1LeLUgTfPN7sLPhMubx0PLAg== + dependencies: + semver "^5.4.1" + +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= + nopt@3.x: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -926,11 +1130,31 @@ nopt@3.x: dependencies: abbrev "1" +npmlog@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + object-inspect@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" @@ -949,7 +1173,7 @@ object.getownpropertydescriptors@^2.0.3: define-properties "^1.1.2" es-abstract "^1.5.1" -once@1.x, once@^1.3.0: +once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -1002,6 +1226,27 @@ postinstall-build@^5.0.1: resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg== +prebuild-install@5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.3.tgz#ef4052baac60d465f5ba6bf003c9c1de79b9da8e" + integrity sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.0" + mkdirp "^0.5.1" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -1012,6 +1257,11 @@ process-nextick-args@~1.0.6: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + psl@^1.1.24: version "1.1.31" resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" @@ -1022,6 +1272,14 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -1032,11 +1290,48 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@^6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -1092,11 +1387,16 @@ resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -safe-buffer@^5.0.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -1107,6 +1407,16 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + should-equal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" @@ -1151,6 +1461,25 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" + integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= + +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + source-map-support@^0.5.12: version "0.5.16" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" @@ -1191,6 +1520,23 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string.prototype.trimleft@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" @@ -1207,11 +1553,32 @@ string.prototype.trimright@^2.1.0: define-properties "^1.1.3" function-bind "^1.1.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" @@ -1219,6 +1586,11 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + supports-color@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" @@ -1233,6 +1605,27 @@ supports-color@^3.1.0: dependencies: has-flag "^1.0.0" +tar-fs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" + integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== + dependencies: + chownr "^1.1.1" + mkdirp "^0.5.1" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" + integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== + dependencies: + bl "^4.0.1" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + through2@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" @@ -1315,7 +1708,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -1358,6 +1751,11 @@ vscode-nls@^4.0.0: remap-istanbul "^0.11.1" source-map-support "^0.5.12" +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + which@^1.1.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -1365,6 +1763,13 @@ which@^1.1.1: dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 2341b457e9..182345b498 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -2209,7 +2209,10 @@ declare module 'azdata' { export enum AzureResource { ResourceManagement = 0, - Sql = 1 + Sql = 1, + OssRdbms = 2, + AzureKeyVault = 3, + Graph = 4 } export interface DidChangeAccountsParams { @@ -2272,7 +2275,7 @@ declare module 'azdata' { * @param resource The resource to get the token for * @return Promise to return a security token object */ - getSecurityToken(account: Account, resource: AzureResource): Thenable<{}>; + getSecurityToken(account: Account, resource: AzureResource): Thenable<{} | undefined>; /** * Prompts the user to enter account information. diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 5a7709a726..c1de32754c 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -125,14 +125,6 @@ declare module 'azdata' { defaultValueOsOverrides?: DefaultValueOsOverride[]; } - /* - * Add OssRdbms for sqlops AzureResource. - */ - export enum AzureResource { - OssRdbms = 2, - AzureKeyVault = 3 - } - export interface ModelBuilder { radioCardGroup(): ComponentBuilder; separator(): ComponentBuilder; diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 02176d4044..2b544879b4 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -395,7 +395,8 @@ export enum AzureResource { ResourceManagement = 0, Sql = 1, OssRdbms = 2, - AzureKeyVault = 3 + AzureKeyVault = 3, + Graph = 4 } export class TreeItem extends vsExtTypes.TreeItem {