mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-03 17:23:42 -05:00
New Azure Auth Provider (#9664)
* Fixes how azure auth is handled on the azure pane * New auth provider * Add externals * Feedback * Change azdata * Fix other issues * Review feedback * Feedback * Feedback updates * Move AKV to azdata.d.ts * Fix yarn.lock * Update third party notices
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -13,5 +13,8 @@ module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
},
|
||||
externals: {
|
||||
'keytar': 'commonjs keytar'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
504
extensions/azurecore/src/account-provider/auths/azureAuth.ts
Normal file
504
extensions/azurecore/src/account-provider/auths/azureAuth.ts
Normal file
@@ -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<AzureAccount | azdata.PromptFailedResult>;
|
||||
|
||||
public abstract async autoOAuthCancelled(): Promise<void>;
|
||||
|
||||
|
||||
public async refreshAccess(account: azdata.Account): Promise<azdata.Account> {
|
||||
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<TokenResponse | undefined> {
|
||||
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<void> {
|
||||
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<AxiosResponse<any>> {
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
return axios.get(uri, config);
|
||||
}
|
||||
|
||||
protected async getTenants(token: AccessToken): Promise<Tenant[]> {
|
||||
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<Subscription[]> {
|
||||
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<TokenRefreshResponse | undefined> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
const results = await this.tokenCache.findCredentials(account.accountId);
|
||||
|
||||
for (let { account } of results) {
|
||||
await this.tokenCache.clearCredential(account);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAllCache(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
return this.server.shutdown();
|
||||
}
|
||||
|
||||
public async login(): Promise<azdata.Account | azdata.PromptFailedResult> {
|
||||
let authCompleteDeferred: Deferred<void>;
|
||||
let authCompletePromise = new Promise<void>((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<void>): Promise<string> {
|
||||
const mediaPath = path.join(this.context.extensionPath, 'media');
|
||||
|
||||
// Utility function
|
||||
const sendFile = async (res: http.ServerResponse, filePath: string, contentType: string): Promise<void> => {
|
||||
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<string>((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<TokenRefreshResponse | undefined> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<AzureAccount | azdata.PromptFailedResult> {
|
||||
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<DeviceCodeLoginResult> {
|
||||
const timeoutMessage = localize('azure.timeoutDeviceCode', 'Timed out when waiting for device code login.');
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
|
||||
return new Promise<DeviceCodeLoginResult | undefined>((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<DeviceCodeLoginResult> {
|
||||
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<void> {
|
||||
return azdata.accounts.endAutoOAuthDeviceCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<AzureAuthType, AzureAuth>();
|
||||
private initComplete: Deferred<void>;
|
||||
private initCompletePromise: Promise<void> = new Promise<void>((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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
return this._tokenCache.clear();
|
||||
}
|
||||
|
||||
public getSecurityToken(account: AzureAccount, resource: azdata.AzureResource): Thenable<AzureAccountSecurityTokenCollection> {
|
||||
return this.doIfInitialized(() => this.getAccessTokens(account, resource));
|
||||
}
|
||||
|
||||
public initialize(restoredAccounts: azdata.Account[]): Thenable<azdata.Account[]> {
|
||||
let self = this;
|
||||
|
||||
let rehydrationTasks: Thenable<azdata.Account>[] = [];
|
||||
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<AzureAccount | azdata.PromptFailedResult> {
|
||||
return this.doIfInitialized(() => this.signIn(true));
|
||||
}
|
||||
|
||||
public refresh(account: AzureAccount): Thenable<AzureAccount | azdata.PromptFailedResult> {
|
||||
return this.doIfInitialized(() => this.signIn(false));
|
||||
}
|
||||
|
||||
// PRIVATE METHODS /////////////////////////////////////////////////////
|
||||
private cancelAutoOAuth(): Promise<void> {
|
||||
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<void> {
|
||||
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<azdata.Account[]> {
|
||||
return this._initialize(storedAccounts);
|
||||
}
|
||||
|
||||
private async _initialize(storedAccounts: azdata.Account[]): Promise<azdata.Account[]> {
|
||||
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<TokenResponse | undefined> {
|
||||
return this._getSecurityToken(account, resource);
|
||||
}
|
||||
|
||||
private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise<TokenResponse | undefined> {
|
||||
await this.initCompletePromise;
|
||||
const azureAuth = this.getAuthMethod(undefined);
|
||||
return azureAuth?.getSecurityToken(account, resource);
|
||||
}
|
||||
|
||||
prompt(): Thenable<azdata.Account | azdata.PromptFailedResult> {
|
||||
return this._prompt();
|
||||
}
|
||||
|
||||
private async _prompt(): Promise<azdata.Account | azdata.PromptFailedResult> {
|
||||
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<azdata.Account | azdata.PromptFailedResult> {
|
||||
return this.prompt();
|
||||
}
|
||||
|
||||
clear(accountKey: azdata.AccountKey): Thenable<void> {
|
||||
return this._clear(accountKey);
|
||||
}
|
||||
|
||||
private async _clear(accountKey: azdata.AccountKey): Promise<void> {
|
||||
await this.initCompletePromise;
|
||||
await this.getAuthMethod(undefined)?.clearCredentials(accountKey);
|
||||
}
|
||||
|
||||
autoOAuthCancelled(): Thenable<void> {
|
||||
this.authMappings.forEach(val => val.autoOAuthCancelled());
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async clearAccountTokens(accountKey: azdata.AccountKey): Promise<void> {
|
||||
// Put together a query to look up any tokens associated with the account key
|
||||
let query = <adal.TokenResponse>{ 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<T>(op: () => Promise<T>): Promise<T> {
|
||||
return this._isInitialized
|
||||
? op()
|
||||
: Promise.reject(localize('accountProviderNotInitialized', "Account provider not initialized, cannot perform action"));
|
||||
}
|
||||
|
||||
private getAccessTokens(account: AzureAccount, resource: azdata.AzureResource): Promise<AzureAccountSecurityTokenCollection> {
|
||||
let self = this;
|
||||
|
||||
const resourceIdMap = new Map<azdata.AzureResource, string>([
|
||||
[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<void>[] = [];
|
||||
let tokenCollection: AzureAccountSecurityTokenCollection = {};
|
||||
for (let tenant of account.properties.tenants) {
|
||||
let promise = new Promise<void>((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 = <adal.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<InProgressAutoOAuth> {
|
||||
let self = this;
|
||||
|
||||
// Create authentication context and acquire user code
|
||||
return new Promise<InProgressAutoOAuth>((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<adal.TokenResponse | azdata.PromptFailedResult> {
|
||||
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<adal.TokenResponse | azdata.PromptFailedResult>((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(<adal.TokenResponse>response);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getTenants(userId: string, homeTenant: string): Thenable<Tenant[]> {
|
||||
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<Tenant>[] = 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 <Tenant>{
|
||||
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<adal.TokenResponse> {
|
||||
let self = this;
|
||||
|
||||
return new Promise<adal.TokenResponse>((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(<adal.TokenResponse>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<any> {
|
||||
return new Promise<any>((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 && (<azdata.PromptFailedResult>value).canceled;
|
||||
}
|
||||
|
||||
private async signIn(isAddAccount: boolean): Promise<AzureAccount | azdata.PromptFailedResult> {
|
||||
// 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 <AzureAccount>{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
return this._tokenCache.clear();
|
||||
}
|
||||
|
||||
// interface method
|
||||
initialize(storedAccounts: azdata.Account[]): Thenable<azdata.Account[]> {
|
||||
return this._initialize(storedAccounts);
|
||||
}
|
||||
|
||||
private async _initialize(storedAccounts: azdata.Account[]): Promise<azdata.Account[]> {
|
||||
for (let account of storedAccounts) {
|
||||
try {
|
||||
await this.getAccessTokens(account, azdata.AzureResource.ResourceManagement);
|
||||
} catch (e) {
|
||||
console.error(`Refreshing account ${account.displayInfo} failed - ${e}`);
|
||||
account.isStale = true;
|
||||
azdata.accounts.accountUpdated(account);
|
||||
}
|
||||
}
|
||||
this.isInitialized = true;
|
||||
return storedAccounts;
|
||||
}
|
||||
|
||||
private async getToken(userId: string, tenantId: string, resourceId: string): Promise<TokenResponse> {
|
||||
let authorityUrl = url.resolve(this.metadata.settings.host, tenantId);
|
||||
const context = new AuthenticationContext(authorityUrl, null, this._tokenCache);
|
||||
|
||||
const acquireToken = promisify(context.acquireToken).bind(context);
|
||||
|
||||
let response: (TokenResponse | ErrorResponse) = await acquireToken(resourceId, userId, this.metadata.settings.clientId);
|
||||
if (response.error) {
|
||||
throw new Error(`Response contained error ${response}`);
|
||||
}
|
||||
|
||||
response = response as TokenResponse;
|
||||
|
||||
context.cache.add([response], (err, result) => {
|
||||
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<AzureAccountSecurityTokenCollection> {
|
||||
const resourceIdMap = new Map<azdata.AzureResource, string>([
|
||||
[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<azdata.Account | azdata.PromptFailedResult> {
|
||||
return this._prompt();
|
||||
}
|
||||
|
||||
|
||||
private async _prompt(): Promise<azdata.Account | azdata.PromptFailedResult> {
|
||||
if (this.isInitialized === false) {
|
||||
vscode.window.showInformationMessage(notInitalizedMessage);
|
||||
return { canceled: false };
|
||||
}
|
||||
const pathMappings = new Map<string, (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => void>();
|
||||
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
const server = this.createAuthServer(pathMappings);
|
||||
|
||||
const port = await this.listenToServer(server);
|
||||
try {
|
||||
const authUrl = this.createAuthUrl(
|
||||
this.metadata.settings.host,
|
||||
AzureAccountProvider.redirectUrlAAD,
|
||||
this.metadata.settings.clientId,
|
||||
this.metadata.settings.signInResourceId,
|
||||
AzureAccountProvider.AadCommonTenant,
|
||||
`${port},${encodeURIComponent(nonce)}`
|
||||
);
|
||||
|
||||
this.addServerPaths(pathMappings, nonce, authUrl);
|
||||
|
||||
const accountAuthenticatedPromise = new Promise<AzureAccount>((resolve, reject) => {
|
||||
AzureAccountProvider.eventEmitter.on(AzureAccountProvider.AzureAccountAuthenticatedEvent, ({ account, error }) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(account);
|
||||
});
|
||||
});
|
||||
|
||||
const urlToOpen = `http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`;
|
||||
|
||||
vscode.env.openExternal(vscode.Uri.parse(urlToOpen));
|
||||
|
||||
const account = await accountAuthenticatedPromise;
|
||||
|
||||
return account;
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
||||
private addServerPaths(
|
||||
pathMappings: Map<string, (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => void>,
|
||||
nonce: string,
|
||||
authUrl: string) {
|
||||
|
||||
const mediaPath = path.join(this._context.extensionPath, 'media');
|
||||
|
||||
// Utility function
|
||||
const sendFile = async (res: http.ServerResponse, filePath: string, contentType: string): Promise<void> => {
|
||||
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<any> {
|
||||
const params = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken.accessToken}`
|
||||
},
|
||||
json: true
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.get(uri, params, (error: any, response: request.Response, body: any) => {
|
||||
const err = error ?? body.error;
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(body.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getTenants(userId: string, homeTenant?: string): Promise<Tenant[]> {
|
||||
const armToken = await this.getToken(userId, AzureAccountProvider.AadCommonTenant, this.metadata.settings.armResource.id);
|
||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2015-01-01');
|
||||
const armWebResponse: any[] = await this.makeWebRequest(armToken, tenantUri);
|
||||
|
||||
const promises = armWebResponse.map(async (value: { tenantId: string }) => {
|
||||
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<void> {
|
||||
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<TokenResponse> {
|
||||
const context = new AuthenticationContext(this.commonAuthorityUrl, null, this._tokenCache);
|
||||
const acquireToken = promisify(context.acquireTokenWithAuthorizationCode).bind(context);
|
||||
|
||||
let token = await acquireToken(code, redirectUrl, this.metadata.settings.signInResourceId, this.metadata.settings.clientId, undefined);
|
||||
if (token.error) {
|
||||
throw new Error(`${token.error} - ${token.errorDescription}`);
|
||||
}
|
||||
token = token as TokenResponse;
|
||||
token._clientId = this.metadata.settings.clientId;
|
||||
token._authority = this.commonAuthorityUrl;
|
||||
token.isMRRT = true;
|
||||
|
||||
context.cache.add([token], (err, result) => {
|
||||
console.log(err, result);
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private createAuthUrl(baseHost: string, redirectUri: string, clientId: string, resource: string, tenant: string, nonce: string): string {
|
||||
return `${baseHost}${encodeURIComponent(tenant)}/oauth2/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${nonce}&resource=${encodeURIComponent(resource)}&prompt=select_account`;
|
||||
}
|
||||
|
||||
private createAuthServer(pathMappings: Map<string, (req: http.IncomingMessage, res: http.ServerResponse, reqUrl: url.UrlWithParsedQuery) => void>) {
|
||||
const server = http.createServer((req, res) => {
|
||||
// Parse URL and the query string
|
||||
const reqUrl = url.parse(req.url, true);
|
||||
|
||||
const method = pathMappings.get(reqUrl.pathname);
|
||||
if (method) {
|
||||
method(req, res, reqUrl);
|
||||
} else {
|
||||
console.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<number> {
|
||||
let portTimer: NodeJS.Timer;
|
||||
const cancelPortTimer = (() => {
|
||||
clearTimeout(portTimer);
|
||||
});
|
||||
|
||||
const port = new Promise<number>((resolve, reject) => {
|
||||
// If no port for 5 seconds, reject it.
|
||||
portTimer = setTimeout(() => {
|
||||
reject(new Error('Timeout waiting for port'));
|
||||
}, 5000);
|
||||
|
||||
server.on('listening', () => {
|
||||
const address = server.address() as AddressInfo;
|
||||
if (address!.port === undefined) {
|
||||
reject(new Error('Port was not defined'));
|
||||
}
|
||||
resolve(address.port);
|
||||
});
|
||||
server.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
server.on('close', () => {
|
||||
reject(new Error('Closed'));
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
});
|
||||
|
||||
const portValue = await port;
|
||||
cancelPortTimer();
|
||||
|
||||
return portValue;
|
||||
}
|
||||
|
||||
// interface method
|
||||
refresh(account: azdata.Account): Thenable<azdata.Account | azdata.PromptFailedResult> {
|
||||
return this._refresh(account);
|
||||
}
|
||||
|
||||
private async _refresh(account: azdata.Account): Promise<azdata.Account | azdata.PromptFailedResult> {
|
||||
return this.prompt();
|
||||
}
|
||||
|
||||
// interface method
|
||||
clear(accountKey: azdata.AccountKey): Thenable<void> {
|
||||
return this._clear(accountKey);
|
||||
}
|
||||
|
||||
private async _clear(accountKey: azdata.AccountKey): Promise<void> {
|
||||
// Put together a query to look up any tokens associated with the account key
|
||||
let query = { userId: accountKey.accountId } as TokenResponse;
|
||||
|
||||
// 1) Look up the tokens associated with the query
|
||||
// 2) Remove them
|
||||
let results = await this._tokenCache.findThenable(query);
|
||||
this._tokenCache.removeThenable(results);
|
||||
}
|
||||
|
||||
// interface method
|
||||
autoOAuthCancelled(): Thenable<void> {
|
||||
return this._autoOAuthCancelled();
|
||||
}
|
||||
|
||||
private async _autoOAuthCancelled(): Promise<void> {
|
||||
// I don't think we need this?
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
// 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<void>[] = [];
|
||||
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<boolean>(provider.configKey)
|
||||
: false;
|
||||
let newConfigValue = newConfig.get<boolean>(provider.configKey);
|
||||
// Determine what providers need to be changed
|
||||
let providerChanges: Promise<void>[] = [];
|
||||
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<boolean>(provider.configKey)
|
||||
: false;
|
||||
let newConfigValue = newConfig.get<boolean>(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<void> {
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> {
|
||||
resolve: (result: T | Promise<T>) => void;
|
||||
reject: (reason: any) => void;
|
||||
}
|
||||
|
||||
@@ -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/'
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
187
extensions/azurecore/src/account-provider/simpleTokenCache.ts
Normal file
187
extensions/azurecore/src/account-provider/simpleTokenCache.ts
Normal file
@@ -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<Keytar | undefined> {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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<string> {
|
||||
return db.get(`${service}${separator}${account}`);
|
||||
},
|
||||
|
||||
async setPassword(service: string, account: string, password: string): Promise<void> {
|
||||
await db.set(`${service}${separator}${account}`, password);
|
||||
},
|
||||
|
||||
async deletePassword(service: string, account: string): Promise<boolean> {
|
||||
await db.delete(`${service}${separator}${account}`);
|
||||
return true;
|
||||
},
|
||||
|
||||
async getPasswords(service: string): Promise<MultipleAccountsResponse> {
|
||||
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<MultipleAccountsResponse>;
|
||||
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<void> {
|
||||
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<MultipleAccountsResponse> => {
|
||||
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<void> {
|
||||
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<string | undefined> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AzureAccount | PromptFailedResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public autoOAuthCancelled(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
7
src/sql/azdata.d.ts
vendored
7
src/sql/azdata.d.ts
vendored
@@ -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.
|
||||
|
||||
8
src/sql/azdata.proposed.d.ts
vendored
8
src/sql/azdata.proposed.d.ts
vendored
@@ -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<RadioCardGroupComponent>;
|
||||
separator(): ComponentBuilder<SeparatorComponent>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user